webtranslateit-safe 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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