webtranslateit-safe 0.4.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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +3 -0
  3. data/.document +5 -0
  4. data/.github/dependabot.yml +26 -0
  5. data/.github/release-drafter.yml +36 -0
  6. data/.github/workflows/ci.yml +51 -0
  7. data/.github/workflows/release-drafter.yml +29 -0
  8. data/.gitignore +18 -0
  9. data/.rspec +3 -0
  10. data/.rubocop.yml +8 -0
  11. data/.rubocop_todo.yml +552 -0
  12. data/CHANGELOG +42 -0
  13. data/Gemfile +11 -0
  14. data/Gemfile.lock +89 -0
  15. data/LICENSE.txt +22 -0
  16. data/README.markdown +237 -0
  17. data/Rakefile +8 -0
  18. data/TODO +31 -0
  19. data/bin/webtranslateit-safe +64 -0
  20. data/lib/extensions/mktmpdir.rb +45 -0
  21. data/lib/webtranslateit/safe/archive.rb +29 -0
  22. data/lib/webtranslateit/safe/backup.rb +27 -0
  23. data/lib/webtranslateit/safe/cloudfiles.rb +77 -0
  24. data/lib/webtranslateit/safe/config/builder.rb +100 -0
  25. data/lib/webtranslateit/safe/config/node.rb +79 -0
  26. data/lib/webtranslateit/safe/ftp.rb +85 -0
  27. data/lib/webtranslateit/safe/gpg.rb +52 -0
  28. data/lib/webtranslateit/safe/gzip.rb +29 -0
  29. data/lib/webtranslateit/safe/local.rb +55 -0
  30. data/lib/webtranslateit/safe/mongodump.rb +30 -0
  31. data/lib/webtranslateit/safe/mysqldump.rb +36 -0
  32. data/lib/webtranslateit/safe/pgdump.rb +36 -0
  33. data/lib/webtranslateit/safe/pipe.rb +23 -0
  34. data/lib/webtranslateit/safe/s3.rb +80 -0
  35. data/lib/webtranslateit/safe/sftp.rb +96 -0
  36. data/lib/webtranslateit/safe/sink.rb +40 -0
  37. data/lib/webtranslateit/safe/source.rb +51 -0
  38. data/lib/webtranslateit/safe/stream.rb +40 -0
  39. data/lib/webtranslateit/safe/svndump.rb +17 -0
  40. data/lib/webtranslateit/safe/tmp_file.rb +53 -0
  41. data/lib/webtranslateit/safe/version.rb +9 -0
  42. data/lib/webtranslateit/safe.rb +70 -0
  43. data/spec/integration/archive_integration_spec.rb +89 -0
  44. data/spec/integration/cleanup_spec.rb +62 -0
  45. data/spec/spec_helper.rb +7 -0
  46. data/spec/webtranslateit/safe/archive_spec.rb +67 -0
  47. data/spec/webtranslateit/safe/cloudfiles_spec.rb +175 -0
  48. data/spec/webtranslateit/safe/config_spec.rb +307 -0
  49. data/spec/webtranslateit/safe/gpg_spec.rb +148 -0
  50. data/spec/webtranslateit/safe/gzip_spec.rb +64 -0
  51. data/spec/webtranslateit/safe/local_spec.rb +109 -0
  52. data/spec/webtranslateit/safe/mongodump_spec.rb +54 -0
  53. data/spec/webtranslateit/safe/mysqldump_spec.rb +83 -0
  54. data/spec/webtranslateit/safe/pgdump_spec.rb +45 -0
  55. data/spec/webtranslateit/safe/s3_spec.rb +168 -0
  56. data/spec/webtranslateit/safe/svndump_spec.rb +39 -0
  57. data/templates/script.rb +183 -0
  58. data/webtranslateit-safe.gemspec +32 -0
  59. metadata +149 -0
@@ -0,0 +1,40 @@
1
+ module WebTranslateIt
2
+
3
+ module Safe
4
+
5
+ class Stream
6
+
7
+ attr_accessor :config, :backup
8
+
9
+ def initialize(config, backup)
10
+ @config = config
11
+ @backup = backup
12
+ end
13
+
14
+ # FIXME: move to Backup
15
+ def expand(path)
16
+ path
17
+ .gsub(/:kind\b/, @backup.kind.to_s)
18
+ .gsub(/:id\b/, @backup.id.to_s)
19
+ .gsub(/:timestamp\b/, @backup.timestamp)
20
+ end
21
+
22
+ private
23
+
24
+ def verbose?
25
+ config[:verbose]
26
+ end
27
+
28
+ def local_only?
29
+ config[:local_only]
30
+ end
31
+
32
+ def dry_run?
33
+ config[:dry_run]
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,17 @@
1
+ module WebTranslateIt
2
+
3
+ module Safe
4
+
5
+ class Svndump < Source
6
+
7
+ def command
8
+ "svnadmin dump #{config[:options]} #{config[:repo_path]}"
9
+ end
10
+
11
+ def extension = '.svn'
12
+
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,53 @@
1
+ require 'tmpdir'
2
+ module WebTranslateIt
3
+
4
+ module Safe
5
+
6
+ module TmpFile
7
+
8
+ @keep_files = []
9
+
10
+ def self.tmproot
11
+ @tmproot ||= Dir.mktmpdir
12
+ end
13
+
14
+ def self.cleanup
15
+ begin
16
+ FileUtils.remove_entry_secure tmproot
17
+ rescue ArgumentError => e
18
+ raise unless e.message =~ /parent directory is world writable/
19
+
20
+ puts <<~ERR
21
+
22
+
23
+ ********************************************************************************
24
+ It looks like you have wrong permissions on your TEMP directory. The usual
25
+ case is when you have world writable TEMP directory withOUT the sticky bit.
26
+
27
+ Try "chmod +t" on it.
28
+
29
+ ********************************************************************************
30
+
31
+ ERR
32
+ raise
33
+ end
34
+ @tmproot = nil
35
+ end
36
+
37
+ def self.create(name)
38
+ # create temp directory
39
+
40
+ file = Tempfile.new(name, tmproot)
41
+
42
+ yield file
43
+
44
+ file.close
45
+ @keep_files << file # so that it will not get gcollected and removed from filesystem until the end
46
+ file.path
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,9 @@
1
+ module WebTranslateIt
2
+
3
+ module Safe
4
+
5
+ VERSION = '0.4.0'
6
+
7
+ end
8
+
9
+ end
@@ -0,0 +1,70 @@
1
+ require 'webtranslateit/safe/version'
2
+
3
+ require 'aws/s3'
4
+ require 'cloudfiles'
5
+ require 'net/sftp'
6
+ # require 'net/ftp'
7
+ require 'fileutils'
8
+ require 'benchmark'
9
+
10
+ require 'tempfile'
11
+ require 'extensions/mktmpdir'
12
+
13
+ require 'webtranslateit/safe/tmp_file'
14
+
15
+ require 'webtranslateit/safe/config/node'
16
+ require 'webtranslateit/safe/config/builder'
17
+
18
+ require 'webtranslateit/safe/stream'
19
+
20
+ require 'webtranslateit/safe/backup'
21
+
22
+ require 'webtranslateit/safe/source'
23
+ require 'webtranslateit/safe/mysqldump'
24
+ require 'webtranslateit/safe/pgdump'
25
+ require 'webtranslateit/safe/archive'
26
+ require 'webtranslateit/safe/svndump'
27
+ require 'webtranslateit/safe/mongodump'
28
+
29
+ require 'webtranslateit/safe/pipe'
30
+ require 'webtranslateit/safe/gpg'
31
+ require 'webtranslateit/safe/gzip'
32
+
33
+ require 'webtranslateit/safe/sink'
34
+ require 'webtranslateit/safe/local'
35
+ require 'webtranslateit/safe/s3'
36
+ require 'webtranslateit/safe/cloudfiles'
37
+ require 'webtranslateit/safe/sftp'
38
+ require 'webtranslateit/safe/ftp'
39
+
40
+ module WebTranslateIt
41
+
42
+ module Safe
43
+
44
+ ROOT = File.join(File.dirname(__FILE__), '..', '..')
45
+
46
+ def safe(&)
47
+ Config::Node.new(&)
48
+ end
49
+
50
+ def process(config)
51
+ [[Mysqldump, [:mysqldump, :databases]],
52
+ [Pgdump, [:pgdump, :databases]],
53
+ [Mongodump, [:mongodump, :databases]],
54
+ [Archive, [:tar, :archives]],
55
+ [Svndump, [:svndump, :repos]]].each do |klass, path|
56
+ next unless (collection = config[*path])
57
+
58
+ collection.each do |name, c|
59
+ klass.new(name, c).backup.run(c, :gpg, :gzip, :local, :s3, :cloudfiles, :sftp, :ftp)
60
+ end
61
+ end
62
+
63
+ WebTranslateIt::Safe::TmpFile.cleanup
64
+ end
65
+ module_function :safe
66
+ module_function :process
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+
3
+ require 'fileutils'
4
+ include FileUtils
5
+
6
+ describe 'tar backup' do
7
+ before(:all) do
8
+ # need both local and instance vars
9
+ # instance variables are used in tests
10
+ # local variables are used in the backup definition (instance vars can't be seen)
11
+ @root = root = 'tmp/archive_backup_example'
12
+
13
+ # clean state
14
+ rm_rf @root
15
+ mkdir_p @root
16
+
17
+ # create source tree
18
+ @src = src = "#{@root}/src"
19
+ mkdir_p "#{@src}/q/w/e"
20
+ mkdir_p "#{@src}/a/s/d"
21
+
22
+ File.open("#{@src}/qwe1", 'w') {|f| f.write('qwe') }
23
+ File.open("#{@src}/q/qwe2", 'w') {|f| f.write('qwe'*2) }
24
+ File.open("#{@src}/q/w/qwe3", 'w') {|f| f.write('qwe'*3) }
25
+ File.open("#{@src}/q/w/e/qwe4", 'w') {|f| f.write('qwe'*4) }
26
+
27
+ File.open("#{@src}/asd1", 'w') {|f| f.write('asd') }
28
+ File.open("#{@src}/a/asd2", 'w') {|f| f.write('asd' * 2) }
29
+ File.open("#{@src}/a/s/asd3", 'w') {|f| f.write('asd' * 3) }
30
+
31
+ @dst = dst = "#{@root}/backup"
32
+ mkdir_p @dst
33
+
34
+ @now = Time.now
35
+ @timestamp = @now.strftime('%y%m%d-%H%M')
36
+
37
+ stub(Time).now {@now} # Freeze
38
+
39
+ config = WebTranslateIt::Safe.safe do
40
+ local :path => "#{dst}/:kind"
41
+ tar do
42
+ archive :test1 do
43
+ files src
44
+ exclude "#{src}/q/w"
45
+ exclude "#{src}/q/w/e"
46
+ end
47
+ end
48
+ end
49
+ WebTranslateIt::Safe.process config
50
+
51
+ @backup = "#{dst}/archive/archive-test1.#{@timestamp}.tar.gz"
52
+ end
53
+
54
+ it 'should create backup file' do
55
+ File.exist?(@backup).should be true
56
+ end
57
+
58
+ describe 'after extracting' do
59
+ before(:all) do
60
+ # prepare target dir
61
+ @target = "#{@root}/test"
62
+ mkdir_p @target
63
+ system "tar -zxvf #{@backup} -C #{@target}"
64
+
65
+ @test = "#{@target}/#{@root}/src"
66
+ puts @test
67
+ end
68
+
69
+ it 'should include asd1/2/3' do
70
+ File.exist?("#{@test}/asd1").should be true
71
+ File.exist?("#{@test}/a/asd2").should be true
72
+ File.exist?("#{@test}/a/s/asd3").should be true
73
+ end
74
+
75
+ it 'should only include qwe 1 and 2 (no 3)' do
76
+ File.exist?("#{@test}/qwe1").should be true
77
+ File.exist?("#{@test}/q/qwe2").should be true
78
+ File.exist?("#{@test}/q/w/qwe3").should be false
79
+ File.exist?("#{@test}/q/w/e/qwe4").should be false
80
+ end
81
+
82
+ it 'should preserve file content' do
83
+ File.read("#{@test}/qwe1").should == 'qwe'
84
+ File.read("#{@test}/q/qwe2").should == 'qweqwe'
85
+ File.read("#{@test}/a/s/asd3").should == 'asdasdasd'
86
+ end
87
+ end
88
+
89
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ require 'fileutils'
4
+ include FileUtils
5
+
6
+ describe 'tar backup' do
7
+ before(:all) do
8
+ # need both local and instance vars
9
+ # instance variables are used in tests
10
+ # local variables are used in the backup definition (instance vars can't be seen)
11
+ @root = root = 'tmp/cleanup_example'
12
+
13
+ # clean state
14
+ rm_rf @root
15
+ mkdir_p @root
16
+
17
+ # create source tree
18
+ @src = src = "#{@root}/src"
19
+ mkdir_p src
20
+
21
+ File.open(qwe = "#{@src}/qwe", 'w') {|f| f.write('qwe') }
22
+
23
+ @dst = dst = "#{@root}/backup"
24
+ mkdir_p "#{@dst}/archive"
25
+
26
+ @now = Time.now
27
+ @timestamp = @now.strftime('%y%m%d-%H%M')
28
+
29
+ stub(Time).now {@now} # Freeze
30
+
31
+ cp qwe, "#{dst}/archive/archive-foo.000001.tar.gz"
32
+ cp qwe, "#{dst}/archive/archive-foo.000002.tar.gz"
33
+ cp qwe, "#{dst}/archive/archive-foobar.000001.tar.gz"
34
+ cp qwe, "#{dst}/archive/archive-foobar.000002.tar.gz"
35
+
36
+ config = WebTranslateIt::Safe.safe do
37
+ local :path => "#{dst}/:kind"
38
+ tar do
39
+ keep :local => 1 # only leave the latest
40
+ archive :foo do
41
+ files src
42
+ end
43
+ end
44
+ end
45
+ WebTranslateIt::Safe.process config
46
+
47
+ @backup = "#{dst}/archive/archive-foo.#{@timestamp}.tar.gz"
48
+ end
49
+
50
+ it 'should create backup file' do
51
+ File.exist?(@backup).should be true
52
+ end
53
+
54
+ it 'should remove old backups' do
55
+ Dir["#{@dst}/archive/archive-foo.*"].should == [@backup]
56
+ end
57
+
58
+ it 'should NOT remove backups with base having same prefix' do
59
+ Dir["#{@dst}/archive/archive-foobar.*"].sort.should == ["#{@dst}/archive/archive-foobar.000001.tar.gz", "#{@dst}/archive/archive-foobar.000002.tar.gz"]
60
+ end
61
+
62
+ end
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'webtranslateit/safe'
4
+
5
+ RSpec.configure do |config|
6
+ config.mock_with :rr
7
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ describe WebTranslateIt::Safe::Archive do
4
+
5
+ def def_config
6
+ {
7
+ options: 'OPTS',
8
+ files: 'apples',
9
+ exclude: 'oranges'
10
+ }
11
+ end
12
+
13
+ def archive(id = :foo, config = def_config)
14
+ WebTranslateIt::Safe::Archive.new(id, WebTranslateIt::Safe::Config::Node.new(nil, config))
15
+ end
16
+
17
+ after(:each) { WebTranslateIt::Safe::TmpFile.cleanup }
18
+
19
+ describe :backup do
20
+ before(:each) do
21
+ @archive = archive
22
+ stub(@archive).timestamp { 'NOW' }
23
+ end
24
+
25
+ {
26
+ id: 'foo',
27
+ kind: 'archive',
28
+ extension: '.tar',
29
+ filename: 'archive-foo.NOW',
30
+ command: 'tar -cf - OPTS --exclude=oranges apples'
31
+ }.each do |k, v|
32
+ it "sets #{k} to #{v}" do
33
+ @archive.backup.send(k).should == v
34
+ end
35
+ end
36
+ end
37
+
38
+ describe :tar_exclude_files do
39
+ it "returns '' when no excludes" do
40
+ archive(:foo, {}).send(:tar_exclude_files).should == ''
41
+ end
42
+
43
+ it 'accepts single exclude as string' do
44
+ archive(:foo, {exclude: 'bar'}).send(:tar_exclude_files).should == '--exclude=bar'
45
+ end
46
+
47
+ it 'accepts multiple exclude as array' do
48
+ archive(:foo, {exclude: ['foo', 'bar']}).send(:tar_exclude_files).should == '--exclude=foo --exclude=bar'
49
+ end
50
+ end
51
+
52
+ describe :tar_files do
53
+ it 'raises RuntimeError when no files' do
54
+ lambda {
55
+ archive(:foo, {}).send(:tar_files)
56
+ }.should raise_error(RuntimeError, 'missing files for tar')
57
+ end
58
+
59
+ it 'accepts single file as string' do
60
+ archive(:foo, {files: 'foo'}).send(:tar_files).should == 'foo'
61
+ end
62
+
63
+ it 'accepts multiple files as array' do
64
+ archive(:foo, {files: ['foo', 'bar']}).send(:tar_files).should == 'foo bar'
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,175 @@
1
+ require 'spec_helper'
2
+
3
+ describe WebTranslateIt::Safe::Cloudfiles do
4
+
5
+ def def_config
6
+ {
7
+ cloudfiles: {
8
+ container: '_container',
9
+ user: '_user',
10
+ api_key: '_api_key',
11
+ },
12
+ keep: { cloudfiles: 2 }
13
+ }
14
+ end
15
+
16
+ def def_backup(extra = {})
17
+ {
18
+ kind: '_kind',
19
+ filename: '/backup/somewhere/_kind-_id.NOW.bar',
20
+ extension: '.bar',
21
+ id: '_id',
22
+ timestamp: 'NOW'
23
+ }.merge(extra)
24
+ end
25
+
26
+ def cloudfiles(config = def_config, backup = def_backup)
27
+ WebTranslateIt::Safe::Cloudfiles.new(
28
+ WebTranslateIt::Safe::Config::Node.new.merge(config),
29
+ WebTranslateIt::Safe::Backup.new(backup)
30
+ )
31
+ end
32
+
33
+ describe :cleanup do
34
+
35
+ before(:each) do
36
+ @cloudfiles = cloudfiles
37
+
38
+ @files = [4,1,3,2].map { |i| "aaaaa#{i}" }
39
+
40
+ @container = 'container'
41
+
42
+ stub(@container).objects(prefix: '_kind/_id/_kind-_id.') { @files }
43
+ stub(@container).delete_object(anything)
44
+
45
+ stub(CloudFiles::Connection).
46
+ new('_user', '_api_key', true, false).stub!.
47
+ container('_container') {@container}
48
+ end
49
+
50
+ it 'should check [:keep, :cloudfiles]' do
51
+ @cloudfiles.config[:keep].data['cloudfiles'] = nil
52
+ dont_allow(@cloudfiles.backup).filename
53
+ @cloudfiles.send :cleanup
54
+ end
55
+
56
+ it 'should delete extra files' do
57
+ mock(@container).delete_object('aaaaa1')
58
+ mock(@container).delete_object('aaaaa2')
59
+ @cloudfiles.send :cleanup
60
+ end
61
+
62
+ end
63
+
64
+ describe :active do
65
+ before(:each) do
66
+ @cloudfiles = cloudfiles
67
+ end
68
+
69
+ it 'should be true when all params are set' do
70
+ expect(@cloudfiles.active?).to be_truthy
71
+ end
72
+
73
+ it 'should be false if container is missing' do
74
+ @cloudfiles.config[:cloudfiles].data['container'] = nil
75
+ expect(@cloudfiles.active?).to be_falsy
76
+ end
77
+
78
+ it 'should be false if user is missing' do
79
+ @cloudfiles.config[:cloudfiles].data['user'] = nil
80
+ expect(@cloudfiles.active?).to be_falsy
81
+ end
82
+
83
+ it 'should be false if api_key is missing' do
84
+ @cloudfiles.config[:cloudfiles].data['api_key'] = nil
85
+ expect(@cloudfiles.active?).to be_falsy
86
+ end
87
+ end
88
+
89
+ describe :path do
90
+ before(:each) do
91
+ @cloudfiles = cloudfiles
92
+ end
93
+ it 'should use cloudfiles/path 1st' do
94
+ @cloudfiles.config[:cloudfiles].data['path'] = 'cloudfiles_path'
95
+ @cloudfiles.config[:local] = {path: 'local_path'}
96
+ @cloudfiles.send(:path).should == 'cloudfiles_path'
97
+ end
98
+
99
+ it 'should use local/path 2nd' do
100
+ @cloudfiles.config.merge local: {path: 'local_path'}
101
+ @cloudfiles.send(:path).should == 'local_path'
102
+ end
103
+
104
+ it 'should use constant 3rd' do
105
+ @cloudfiles.send(:path).should == '_kind/_id'
106
+ end
107
+
108
+ end
109
+
110
+ describe :save do
111
+ def add_stubs(*stubs)
112
+ stubs.each do |s|
113
+ case s
114
+ when :connection
115
+ @connection = 'connection'
116
+ stub(CloudFiles::Authentication).new
117
+ stub(CloudFiles::Connection).
118
+ new('_user', '_api_key', true, false) {@connection}
119
+ when :file_size
120
+ stub(@cloudfiles).get_file_size('foo') {123}
121
+ when :create_container
122
+ @container = 'container'
123
+ stub(@container).create_object('_kind/_id/backup/somewhere/_kind-_id.NOW.bar.bar', true) {@object}
124
+ stub(@connection).create_container {@container}
125
+ when :file_open
126
+ stub(File).open('foo')
127
+ when :cloudfiles_store
128
+ @object = 'object'
129
+ stub(@object).write(nil) {true}
130
+ end
131
+ end
132
+ end
133
+
134
+ before(:each) do
135
+ @cloudfiles = cloudfiles(def_config, def_backup(path: 'foo'))
136
+ @full_path = '_kind/_id/backup/somewhere/_kind-_id.NOW.bar.bar'
137
+ end
138
+
139
+ it 'should fail if no backup.file is set' do
140
+ @cloudfiles.backup.path = nil
141
+ proc {@cloudfiles.send(:save)}.should raise_error(RuntimeError)
142
+ end
143
+
144
+ it 'should establish Cloud Files connection' do
145
+ add_stubs(:connection, :file_size, :create_container, :file_open, :cloudfiles_store)
146
+ @cloudfiles.send(:save)
147
+ end
148
+
149
+ it 'should open local file' do
150
+ add_stubs(:connection, :file_size, :create_container, :cloudfiles_store)
151
+ mock(File).open('foo')
152
+ @cloudfiles.send(:save)
153
+ end
154
+
155
+ it "should call write on the cloudfile object with files' descriptor" do
156
+ add_stubs(:connection, :file_size, :create_container, :cloudfiles_store)
157
+ stub(File).open('foo') {'qqq'}
158
+ mock(@object).write('qqq') {true}
159
+ @cloudfiles.send(:save)
160
+ end
161
+
162
+ it 'should upload file' do
163
+ add_stubs(:connection, :file_size, :create_container, :file_open, :cloudfiles_store)
164
+ @cloudfiles.send(:save)
165
+ end
166
+
167
+ it 'should fail on files bigger then 5G' do
168
+ add_stubs(:connection)
169
+ mock(File).stat('foo').stub!.size {5*1024*1024*1024+1}
170
+ mock(STDERR).puts(anything)
171
+ dont_allow(Benchmark).realtime
172
+ @cloudfiles.send(:save)
173
+ end
174
+ end
175
+ end