sg_tiny_backup 1.0.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +51 -0
  3. data/.gitignore +14 -0
  4. data/.octocov.yml +15 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +70 -0
  7. data/.ruby-version +1 -0
  8. data/.simplecov +14 -0
  9. data/CHANGELOG.md +73 -0
  10. data/Gemfile +23 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +111 -0
  14. data/Rakefile +12 -0
  15. data/bin/console +15 -0
  16. data/bin/setup +8 -0
  17. data/config/database.ci.yml +15 -0
  18. data/lib/sg_tiny_backup/commands/aws_cli.rb +32 -0
  19. data/lib/sg_tiny_backup/commands/base.rb +23 -0
  20. data/lib/sg_tiny_backup/commands/gzip.rb +20 -0
  21. data/lib/sg_tiny_backup/commands/mysql_dump.rb +38 -0
  22. data/lib/sg_tiny_backup/commands/openssl.rb +40 -0
  23. data/lib/sg_tiny_backup/commands/pg_dump.rb +38 -0
  24. data/lib/sg_tiny_backup/commands/tar.rb +68 -0
  25. data/lib/sg_tiny_backup/commands.rb +8 -0
  26. data/lib/sg_tiny_backup/config.rb +71 -0
  27. data/lib/sg_tiny_backup/error.rb +25 -0
  28. data/lib/sg_tiny_backup/pipeline.rb +62 -0
  29. data/lib/sg_tiny_backup/pipeline_builders/base.rb +37 -0
  30. data/lib/sg_tiny_backup/pipeline_builders/db.rb +70 -0
  31. data/lib/sg_tiny_backup/pipeline_builders/log.rb +36 -0
  32. data/lib/sg_tiny_backup/pipeline_builders/s3.rb +26 -0
  33. data/lib/sg_tiny_backup/railtie.rb +9 -0
  34. data/lib/sg_tiny_backup/runner.rb +77 -0
  35. data/lib/sg_tiny_backup/spawner.rb +113 -0
  36. data/lib/sg_tiny_backup/templates/sg_tiny_backup.yml +43 -0
  37. data/lib/sg_tiny_backup/utils.rb +34 -0
  38. data/lib/sg_tiny_backup/version.rb +5 -0
  39. data/lib/sg_tiny_backup.rb +21 -0
  40. data/lib/tasks/sg_tiny_backup.rake +78 -0
  41. data/sg_tiny_backup.gemspec +38 -0
  42. metadata +87 -0
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require "sg_tiny_backup/error"
5
+
6
+ module SgTinyBackup
7
+ class Spawner
8
+ attr_reader :stdout, :stderr, :exit_code_errors
9
+
10
+ def initialize(commands:, env: {}, output_path: nil)
11
+ @commands = commands
12
+ @env = env
13
+ @output_path = output_path
14
+ @stderr_message = nil
15
+ @exit_code_errors = []
16
+ end
17
+
18
+ def succeeded?
19
+ exit_code_errors.empty?
20
+ end
21
+
22
+ def stderr_message
23
+ @stderr_message ||=
24
+ if @stderr.empty?
25
+ ""
26
+ else
27
+ <<~END_OF_MESSAGE
28
+ STDERR messages:
29
+
30
+ #{@stderr}
31
+ END_OF_MESSAGE
32
+ end
33
+ end
34
+
35
+ def exit_code_error_message
36
+ if succeeded?
37
+ ""
38
+ else
39
+ <<~END_OF_MESSAGE
40
+ The following errors were returned:
41
+
42
+ #{@exit_code_errors.join("\n")}
43
+ END_OF_MESSAGE
44
+ end
45
+ end
46
+
47
+ def spawn_and_wait
48
+ spawn_pipeline_command do |pid, out_r, err_r, status_r|
49
+ @stdout = out_r.read
50
+ @stderr = err_r.read
51
+ pipe_status_str = status_r.read
52
+ Process.wait(pid)
53
+ parse_pipe_status(pipe_status_str)
54
+ end
55
+ rescue StandardError => e
56
+ raise SpawnError.new("Pipeline failed to execute", e)
57
+ end
58
+
59
+ private
60
+
61
+ def spawn_pipeline_command
62
+ opts = {}
63
+ out_r, out_w = IO.pipe
64
+ opts[:out] = out_w
65
+
66
+ err_r, err_w = IO.pipe
67
+ opts[:err] = err_w
68
+
69
+ status_r, status_w = IO.pipe
70
+ opts[3] = status_w
71
+
72
+ pid = spawn(@env, pipeline_command, opts)
73
+ out_w.close
74
+ err_w.close
75
+ status_w.close
76
+
77
+ yield pid, out_r, err_r, status_r
78
+ end
79
+
80
+ # pipe_status_str format:
81
+ # {command index}|{exit status}:\n{command index}|{exit status}:\n
82
+ #
83
+ # For example, "0|2:\n1|0:\n" means:
84
+ # * command 0 exited with status 2
85
+ # * command 1 exited with status 0
86
+ def parse_pipe_status(pipe_status_str)
87
+ @exit_code_errors = []
88
+ pipe_statuses = pipe_status_str.delete("\n").split(":").sort
89
+ pipe_statuses.each do |status|
90
+ index, exit_code = status.split("|").map(&:to_i)
91
+ command = @commands[index]
92
+ next if command.success_codes.member?(exit_code)
93
+
94
+ @exit_code_errors << "`#{command.command}` returned exit code: #{exit_code}"
95
+ end
96
+ end
97
+
98
+ # Pipeline command example:
99
+ #
100
+ # { tar -c log/production.log log/production.log.1 ; echo "0|$?:" >&3 ; } | { gzip ; echo "1|$?:" >&3 ; } > log.tar.gz
101
+ #
102
+ # This output each command's exit status to file descriptor 3.
103
+ def pipeline_command
104
+ parts = []
105
+ @commands.each_with_index do |command, index|
106
+ parts << %({ #{command.command} ; echo "#{index}|$?:" >&3 ; })
107
+ end
108
+ command = parts.join(" | ")
109
+ command += " > #{Shellwords.escape(@output_path)}" if @output_path
110
+ command
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,43 @@
1
+ s3:
2
+ db:
3
+ # Destination url is `s3://{BUCKET}/{PREFIX}_{TIMESTAMP}.sql.gz.enc`.
4
+ bucket: YOUR_S3_BUCKET_NAME
5
+ prefix: YOUR_PROJECT/YOUR_PROJECT
6
+ access_key_id: <%= ENV.fetch('AWS_ACCESS_KEY_ID') %>
7
+ secret_access_key: <%= ENV.fetch('AWS_SECRET_ACCESS_KEY') %>
8
+ # If your backup file size exceeds 50GB, you must specify `expected_upload_size`.
9
+ # It must be larger than your backup file size.
10
+ # It is passed to `aws s3 cp`'s `--expected-size` option.
11
+ # See https://docs.aws.amazon.com/cli/latest/reference/s3/cp.html
12
+ #
13
+ # It is used for the calculation of S3 multipart upload chunk size not to exceed the part number limit.
14
+ # See https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
15
+ #
16
+ # expected_upload_size: 100000000000
17
+ log:
18
+ # Destination url is `s3://{BUCKET}/{PREFIX}_{HOSTNAME}_{TIMESTAMP}.tar.gz`.
19
+ bucket: YOUR_S3_BUCKET_NAME
20
+ prefix: YOUR_PROJECT/YOUR_PROJECT
21
+ access_key_id: <%= ENV.fetch('AWS_ACCESS_KEY_ID') %>
22
+ secret_access_key: <%= ENV.fetch('AWS_SECRET_ACCESS_KEY') %>
23
+ # expected_upload_size: 100000000000
24
+ # You can generate ENCRYPTION_KEY with `bundle exec rails secret`.
25
+ encryption_key: <%= ENV.fetch('ENCRYPTION_KEY') %>
26
+ log:
27
+ files:
28
+ - log/production.log
29
+ optional_files:
30
+ - log/production.log.1
31
+ pg_dump:
32
+ extra_options: -xc --if-exists --encoding=utf8
33
+ mysqldump:
34
+ extra_options: --single-transaction --quick --hex-blob
35
+ # gzip:
36
+ # level: 6 # 1 (fastest) to 9 (best compression), default: 6
37
+ # The following settings is not required since database config can be loaded from config/database.yml in typical rails application.
38
+ # db:
39
+ # adapter: postgresql
40
+ # host: localhost
41
+ # port: 5432
42
+ # username: postgres
43
+ # password: YOUR_DB_PASSWORD
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "yaml"
5
+ require "erb"
6
+
7
+ module SgTinyBackup
8
+ module Utils
9
+ class << self
10
+ def timestamp
11
+ Time.now.strftime("%Y%m%d_%H%M%S")
12
+ end
13
+
14
+ def basename(target)
15
+ if target == "log"
16
+ "#{Socket.gethostname}_#{timestamp}"
17
+ else
18
+ timestamp
19
+ end
20
+ end
21
+
22
+ def load_yaml_with_erb(io)
23
+ yaml_content = io.read
24
+ resolved = ERB.new(yaml_content).result
25
+ YAML.safe_load(
26
+ resolved,
27
+ permitted_classes: [],
28
+ permitted_symbols: [],
29
+ aliases: true
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SgTinyBackup
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "sg_tiny_backup/config"
5
+ require "sg_tiny_backup/runner"
6
+ require "sg_tiny_backup/utils"
7
+ require "sg_tiny_backup/version"
8
+ require "sg_tiny_backup/railtie" if defined?(Rails)
9
+
10
+ module SgTinyBackup
11
+ class << self
12
+ attr_accessor :raise_on_error
13
+ attr_writer :logger
14
+
15
+ def logger
16
+ @logger ||= Logger.new(STDOUT)
17
+ end
18
+ end
19
+ end
20
+
21
+ SgTinyBackup.raise_on_error = true
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :sg_tiny_backup do
4
+ default_config_path = ENV["SG_TINY_BACKUP_CONFIG_PATH"] || "config/sg_tiny_backup.yml"
5
+ default_backup_target = ENV["BACKUP_TARGET"] || "db"
6
+
7
+ desc "Generate config file"
8
+ task generate: :environment do
9
+ SgTinyBackup::Config.generate_template_file(default_config_path)
10
+ end
11
+
12
+ desc "Backup and upload to s3"
13
+ task :backup, [:backup_target] => :environment do |_task, args|
14
+ backup_target = args[:backup_target] || default_backup_target
15
+ config = SgTinyBackup::Config.read_file(default_config_path)
16
+ runner = SgTinyBackup::Runner.new(
17
+ config: config,
18
+ target: backup_target,
19
+ basename: SgTinyBackup::Utils.basename(backup_target)
20
+ )
21
+ url = runner.s3_destination_url
22
+ SgTinyBackup.logger.info "Starting backup to #{url}"
23
+ if runner.run
24
+ SgTinyBackup.logger.info "Backup done!"
25
+ else
26
+ SgTinyBackup.logger.error "Backup failed!"
27
+ exit 1
28
+ end
29
+ end
30
+
31
+ desc "Backup to current directory"
32
+ task :backup_local, [:backup_target] => :environment do |_task, args|
33
+ backup_target = args[:backup_target] || default_backup_target
34
+ config = SgTinyBackup::Config.read_file(default_config_path)
35
+ runner = SgTinyBackup::Runner.new(
36
+ config: config,
37
+ target: backup_target,
38
+ basename: SgTinyBackup::Utils.basename(backup_target),
39
+ local: true
40
+ )
41
+ SgTinyBackup.logger.info "Starting backup to #{runner.base_filename}"
42
+ if runner.run
43
+ SgTinyBackup.logger.info "Backup done!"
44
+ else
45
+ SgTinyBackup.logger.error "Backup failed!"
46
+ exit 1
47
+ end
48
+ end
49
+
50
+ desc "Show backup command"
51
+ task :command, [:backup_target] => :environment do |_task, args|
52
+ backup_target = args[:backup_target] || default_backup_target
53
+ config = SgTinyBackup::Config.read_file(default_config_path)
54
+ runner = SgTinyBackup::Runner.new(
55
+ config: config,
56
+ target: backup_target,
57
+ basename: SgTinyBackup::Utils.timestamp
58
+ )
59
+ puts runner.piped_command
60
+ end
61
+
62
+ desc "Show backup command environment variables"
63
+ task :env, [:backup_target] => :environment do |_task, args|
64
+ backup_target = args[:backup_target] || default_backup_target
65
+ config = SgTinyBackup::Config.read_file(default_config_path)
66
+ runner = SgTinyBackup::Runner.new(
67
+ config: config,
68
+ target: backup_target,
69
+ basename: SgTinyBackup::Utils.timestamp
70
+ )
71
+ puts runner.env
72
+ end
73
+
74
+ desc "Show decryption command"
75
+ task decryption_command: :environment do
76
+ puts SgTinyBackup::Commands::Openssl.decryption_command
77
+ end
78
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/sg_tiny_backup/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "sg_tiny_backup"
7
+ spec.version = SgTinyBackup::VERSION
8
+ spec.authors = ["Shunichi Ikegami"]
9
+ spec.email = ["shunichi@sonicgarden.jp"]
10
+
11
+ spec.summary = "Tiny backup"
12
+ spec.description = "Backup postgresql database and logs to S3"
13
+ spec.homepage = "https://github.com/SonicGarden/sg_tiny_backup"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.2.0"
16
+
17
+ # spec.metadata['allowed_push_host'] = "https://rubygems.org"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/SonicGarden/sg_tiny_backup"
21
+ spec.metadata["changelog_uri"] = "https://github.com/SonicGarden/sg_tiny_backup/blob/main/CHANGELOG.md"
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ # Uncomment to register a new dependency of your gem
34
+ # spec.add_dependency "example-gem", "~> 1.0"
35
+
36
+ # For more information and examples about making a new gem, checkout our
37
+ # guide at: https://bundler.io/guides/creating_gem.html
38
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sg_tiny_backup
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Shunichi Ikegami
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-03-03 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Backup postgresql database and logs to S3
14
+ email:
15
+ - shunichi@sonicgarden.jp
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".github/workflows/main.yml"
21
+ - ".gitignore"
22
+ - ".octocov.yml"
23
+ - ".rspec"
24
+ - ".rubocop.yml"
25
+ - ".ruby-version"
26
+ - ".simplecov"
27
+ - CHANGELOG.md
28
+ - Gemfile
29
+ - Gemfile.lock
30
+ - LICENSE.txt
31
+ - README.md
32
+ - Rakefile
33
+ - bin/console
34
+ - bin/setup
35
+ - config/database.ci.yml
36
+ - lib/sg_tiny_backup.rb
37
+ - lib/sg_tiny_backup/commands.rb
38
+ - lib/sg_tiny_backup/commands/aws_cli.rb
39
+ - lib/sg_tiny_backup/commands/base.rb
40
+ - lib/sg_tiny_backup/commands/gzip.rb
41
+ - lib/sg_tiny_backup/commands/mysql_dump.rb
42
+ - lib/sg_tiny_backup/commands/openssl.rb
43
+ - lib/sg_tiny_backup/commands/pg_dump.rb
44
+ - lib/sg_tiny_backup/commands/tar.rb
45
+ - lib/sg_tiny_backup/config.rb
46
+ - lib/sg_tiny_backup/error.rb
47
+ - lib/sg_tiny_backup/pipeline.rb
48
+ - lib/sg_tiny_backup/pipeline_builders/base.rb
49
+ - lib/sg_tiny_backup/pipeline_builders/db.rb
50
+ - lib/sg_tiny_backup/pipeline_builders/log.rb
51
+ - lib/sg_tiny_backup/pipeline_builders/s3.rb
52
+ - lib/sg_tiny_backup/railtie.rb
53
+ - lib/sg_tiny_backup/runner.rb
54
+ - lib/sg_tiny_backup/spawner.rb
55
+ - lib/sg_tiny_backup/templates/sg_tiny_backup.yml
56
+ - lib/sg_tiny_backup/utils.rb
57
+ - lib/sg_tiny_backup/version.rb
58
+ - lib/tasks/sg_tiny_backup.rake
59
+ - sg_tiny_backup.gemspec
60
+ homepage: https://github.com/SonicGarden/sg_tiny_backup
61
+ licenses:
62
+ - MIT
63
+ metadata:
64
+ homepage_uri: https://github.com/SonicGarden/sg_tiny_backup
65
+ source_code_uri: https://github.com/SonicGarden/sg_tiny_backup
66
+ changelog_uri: https://github.com/SonicGarden/sg_tiny_backup/blob/main/CHANGELOG.md
67
+ rubygems_mfa_required: 'true'
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 3.2.0
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.4.19
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: Tiny backup
87
+ test_files: []