sqlite_backups 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5befa74d4c48692314c28f8ca62ba631a87faaff37c3ee83ee792267127bad8b
4
+ data.tar.gz: 04e12d29cc5c7285487fd63374d598d5d2e687c113f2dd560a556701d4a80d40
5
+ SHA512:
6
+ metadata.gz: 6f610817967e118afb908977f1d14c06cacf0400ed9cb202792914181326430f6a8f219f4f8eb74990d04430c3d09d29035ca66ca9c03ce1972564dbbcd0a9b1
7
+ data.tar.gz: 6bd671346cc80c2a21a160d32e74b2d09a20a2b91645416edbeea89490979edd1b72e9177d36631b006d39d999e1054c43078e551b18f77b6c8d861851a5f207
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # SqliteBackups
2
+
3
+ A dead simple Rails engine to backup your Sqlite databases utilizing Active
4
+ Storage.
5
+
6
+ ## Usage
7
+
8
+ To backup a database:
9
+ ```bash
10
+ $ bin/rails backup:[database_name]
11
+ ```
12
+
13
+ Alternatively, you can use a job:
14
+ ```ruby
15
+ Backups::DatabaseJob.perform_later(database_name)
16
+ Backups::AllJob.perform_later
17
+ ```
18
+
19
+ To restore a database:
20
+ ```bash
21
+ $ bin/rails restore:[database_name]
22
+ ```
23
+
24
+ ## Installation
25
+ Add this line to your Gemfile:
26
+
27
+ ```ruby
28
+ gem "sqlite_backups"
29
+ ```
30
+
31
+ And then execute:
32
+ ```bash
33
+ $ bundle
34
+ ```
35
+
36
+ Then run the installer to copy over the migration and mount a route we use for
37
+ restoring:
38
+ ```bash
39
+ $ bin/rails backups:install
40
+ ```
41
+
42
+ These are the available configuration options:
43
+
44
+ ```ruby
45
+ config.backups.storage_service = :backups
46
+ config.backups.retention = 1.day
47
+ ```
48
+
49
+ ## License
50
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,19 @@
1
+ module Backups
2
+ class BackupsController < ActionController::Base
3
+ before_action :verify_token
4
+
5
+ def show
6
+ files = Backup.where(database: params[:name]).map do
7
+ { date: it.created_at, key: it.file.key }
8
+ end
9
+
10
+ render json: { name: params[:name], files: }
11
+ end
12
+
13
+ private
14
+
15
+ def verify_token
16
+ raise(ActiveRecord::RecordNotFound) unless Backups.valid_token?(params[:token])
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ module Backups
2
+ class AllJob < ApplicationJob
3
+ def perform
4
+ Backups.databases.each_key do |name|
5
+ DatabaseJob.perform_later(name)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ module Backups
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,7 @@
1
+ module Backups
2
+ class DatabaseJob < ApplicationJob
3
+ def perform(name)
4
+ Create.new(name).run
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module Backups
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ module Backups
2
+ class Backup < ApplicationRecord
3
+ self.table_name = "backups"
4
+
5
+ validates :database, inclusion: { in: Backups.databases.keys }
6
+
7
+ has_one_attached :file, service: Backups.storage_service, dependent: :purge_later
8
+
9
+ scope :expired,
10
+ -> { where(created_at: ..(Backups.retention || 1.day).ago) }
11
+
12
+ def formated_date
13
+ created_at.strftime("%Y-%m-%d_%H:%M")
14
+ end
15
+ end
16
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ Backups::Engine.routes.draw do
2
+ get "rails/backups/:name", to: "backups#show", as: :backup
3
+ end
@@ -0,0 +1,9 @@
1
+ class CreateSqliteBackupBackups < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :backups do |t|
4
+ t.string :database
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ module Backups
2
+ class Create
3
+ def initialize(name)
4
+ @name = name
5
+ @path = Backups.databases[name.to_s]
6
+ @key = SecureRandom.hex(16)
7
+ end
8
+
9
+ def run
10
+ execute_backup
11
+
12
+ Backup.create(database: name).tap do
13
+ it.file.attach(io: compressed_data, filename: key)
14
+ File.delete(backup_path)
15
+ File.delete("#{backup_path}.gz")
16
+ end
17
+
18
+ expire_old_backups
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :name, :path, :key
24
+
25
+ def execute_backup
26
+ `sqlite3 #{path} '.backup #{backup_path}'`
27
+ end
28
+
29
+ def compressed_data
30
+ gzip_path = "#{backup_path}.gz"
31
+ Zlib::GzipWriter.open(gzip_path) do |gz|
32
+ File.open(backup_path, "rb") do |f|
33
+ IO.copy_stream(f, gz)
34
+ end
35
+ end
36
+ File.open(gzip_path, "rb")
37
+ end
38
+
39
+ def backup_path
40
+ Rails.root.join("tmp/#{name}_backup_#{key}")
41
+ end
42
+
43
+ def expire_old_backups
44
+ Backup.where(database: name).expired.each do
45
+ it.file.purge
46
+ it.destroy
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ module Backups
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Backups
4
+
5
+ config.backups = ActiveSupport::OrderedOptions.new
6
+
7
+ initializer "backups.config" do
8
+ config.backups.each do |name, value|
9
+ Backups.public_send(:"#{name}=", value)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module Backups
2
+ class Railtie < Rails::Railtie # :nodoc:
3
+ rake_tasks do
4
+ load "backups/tasks.rake"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,63 @@
1
+ require "cli/ui"
2
+
3
+ module Backups
4
+ class Restore
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+
9
+ def run
10
+ raise StandardError, "No backups found" if backups["files"].blank?
11
+ raise StandardError, "File not found" unless service.exist?(key)
12
+
13
+ File.open(path, "wb") do |file|
14
+ file.write(ActiveSupport::Gzip.decompress(service.download(key)))
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :name
21
+
22
+ def path
23
+ Backups.databases(env_name: Rails.env)[name.to_s]
24
+ end
25
+
26
+ def service
27
+ ActiveStorage::Blob.services.fetch(Backups.storage_service)
28
+ end
29
+
30
+ def token
31
+ Backups.generate_token
32
+ end
33
+
34
+ def key
35
+ @key ||=
36
+ if backups["files"].count == 1
37
+ backups["files"].first["key"]
38
+ else
39
+ CLI::UI.ask("Pick a backup to restore", options:).then do |date|
40
+ backups["files"].find { it["date"].to_s == date }["key"]
41
+ end
42
+ end
43
+ end
44
+
45
+ def options
46
+ backups[:files].pluck("date").map(&:to_s)
47
+ end
48
+
49
+ def uri
50
+ host = `RAILS_ENV=production bin/rails runner 'puts Rails.application.config.x.url'`
51
+ URI("#{host.strip}/rails/backups/#{name}").
52
+ tap { it.query = URI.encode_www_form(token:) }
53
+ end
54
+
55
+ def backups
56
+ @backups ||= begin
57
+ ActiveSupport.parse_json_times = true
58
+ ActiveSupport::JSON.
59
+ decode(Net::HTTP.get_response(uri).body).with_indifferent_access
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,15 @@
1
+ namespace :backup do
2
+ Rails.application.config_for(:database).keys.each do |name|
3
+ task name => :environment do
4
+ Backups::DatabaseJob.perform_later(name)
5
+ end
6
+ end
7
+ end
8
+
9
+ namespace :restore do
10
+ Rails.application.config_for(:database).keys.each do |name|
11
+ task name => :environment do
12
+ Backups::Restore.new(name).run
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Backups
2
+ VERSION = "0.1.0"
3
+ end
data/lib/backups.rb ADDED
@@ -0,0 +1,36 @@
1
+ require "backups/version"
2
+ require "backups/engine"
3
+ require "backups/railtie"
4
+ require "backups/create"
5
+ require "backups/restore"
6
+
7
+ module Backups
8
+ mattr_accessor :retention, :storage_service
9
+
10
+ class << self
11
+ def databases(env_name: "production")
12
+ ActiveRecord::Base.
13
+ configurations.
14
+ configs_for(env_name: env_name).
15
+ to_h { [ it.name, it.database ] }
16
+ end
17
+
18
+ def generate_token
19
+ verifier.generate(
20
+ SecureRandom.hex(16), expires_in: 5.minutes, purpose: :fetch_backups
21
+ )
22
+ end
23
+
24
+ def valid_token?(token)
25
+ verifier.verify(token, purpose: :fetch_backups).present?
26
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
27
+ false
28
+ end
29
+
30
+ private
31
+
32
+ def verifier
33
+ Backup.generated_token_verifier
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ Description:
2
+ Installs migration and route
3
+
4
+ Example:
5
+ bin/rails generate backups:install
@@ -0,0 +1,9 @@
1
+ class Backups::InstallGenerator < Rails::Generators::Base
2
+ def add_route
3
+ route "mount Backups::Engine => \"/\""
4
+ end
5
+
6
+ def create_migrations
7
+ rails_command "backups:install:migrations", inline: true
8
+ end
9
+ end
@@ -0,0 +1 @@
1
+ require "backups"
@@ -0,0 +1,6 @@
1
+ desc "Copy over the migrations and mount route for backups"
2
+ namespace :backups do
3
+ task install: :environment do
4
+ Rails::Command.invoke :generate, [ "backups:install" ]
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sqlite_backups
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Pezza
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.0.2
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 8.0.2
26
+ - !ruby/object:Gem::Dependency
27
+ name: cli-ui
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ email:
41
+ - pezza@hey.com
42
+ executables: []
43
+ extensions: []
44
+ extra_rdoc_files: []
45
+ files:
46
+ - README.md
47
+ - Rakefile
48
+ - app/controllers/backups/backups_controller.rb
49
+ - app/jobs/backups/all_job.rb
50
+ - app/jobs/backups/application_job.rb
51
+ - app/jobs/backups/database_job.rb
52
+ - app/models/backups/application_record.rb
53
+ - app/models/backups/backup.rb
54
+ - config/routes.rb
55
+ - db/migrate/20250430005926_create_sqlite_backup_backups.rb
56
+ - lib/backups.rb
57
+ - lib/backups/create.rb
58
+ - lib/backups/engine.rb
59
+ - lib/backups/railtie.rb
60
+ - lib/backups/restore.rb
61
+ - lib/backups/tasks.rake
62
+ - lib/backups/version.rb
63
+ - lib/generators/backups/install/USAGE
64
+ - lib/generators/backups/install/install_generator.rb
65
+ - lib/sqlite_backups.rb
66
+ - lib/tasks/backups_tasks.rake
67
+ homepage: https://github.com/npezza93/sqlite_backup
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ rubygems_mfa_required: 'true'
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 3.2.0
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.6.8
87
+ specification_version: 4
88
+ summary: Simple sqlite backup for Rails
89
+ test_files: []