darkofabijan-astrails-safe 0.2.8

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 (41) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +237 -0
  3. data/Rakefile +61 -0
  4. data/bin/astrails-safe +53 -0
  5. data/examples/example_helper.rb +19 -0
  6. data/lib/astrails/safe.rb +61 -0
  7. data/lib/astrails/safe/archive.rb +24 -0
  8. data/lib/astrails/safe/backup.rb +20 -0
  9. data/lib/astrails/safe/cloudfiles.rb +70 -0
  10. data/lib/astrails/safe/config/builder.rb +60 -0
  11. data/lib/astrails/safe/config/node.rb +76 -0
  12. data/lib/astrails/safe/gpg.rb +46 -0
  13. data/lib/astrails/safe/gzip.rb +25 -0
  14. data/lib/astrails/safe/local.rb +70 -0
  15. data/lib/astrails/safe/mysqldump.rb +32 -0
  16. data/lib/astrails/safe/pgdump.rb +36 -0
  17. data/lib/astrails/safe/pipe.rb +17 -0
  18. data/lib/astrails/safe/s3.rb +86 -0
  19. data/lib/astrails/safe/sftp.rb +88 -0
  20. data/lib/astrails/safe/sink.rb +35 -0
  21. data/lib/astrails/safe/source.rb +47 -0
  22. data/lib/astrails/safe/stream.rb +20 -0
  23. data/lib/astrails/safe/svndump.rb +13 -0
  24. data/lib/astrails/safe/tmp_file.rb +48 -0
  25. data/lib/extensions/mktmpdir.rb +45 -0
  26. data/spec/integration/archive_integration_spec.rb +88 -0
  27. data/spec/integration/cleanup_spec.rb +61 -0
  28. data/spec/spec.opts +5 -0
  29. data/spec/spec_helper.rb +16 -0
  30. data/spec/unit/archive_spec.rb +67 -0
  31. data/spec/unit/cloudfiles_spec.rb +170 -0
  32. data/spec/unit/config_spec.rb +213 -0
  33. data/spec/unit/gpg_spec.rb +148 -0
  34. data/spec/unit/gzip_spec.rb +64 -0
  35. data/spec/unit/local_spec.rb +110 -0
  36. data/spec/unit/mysqldump_spec.rb +83 -0
  37. data/spec/unit/pgdump_spec.rb +45 -0
  38. data/spec/unit/s3_spec.rb +160 -0
  39. data/spec/unit/svndump_spec.rb +39 -0
  40. data/templates/script.rb +165 -0
  41. metadata +179 -0
@@ -0,0 +1,17 @@
1
+ module Astrails
2
+ module Safe
3
+ class Pipe < Stream
4
+ # process adds required commands to the current
5
+ # shell command string
6
+ # :active?, :pipe, :extension and :post_process are
7
+ # defined in inheriting pipe classes
8
+ def process
9
+ return unless active?
10
+
11
+ @backup.command << pipe
12
+ @backup.extension << extension
13
+ post_process
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,86 @@
1
+ module Astrails
2
+ module Safe
3
+ class S3 < Sink
4
+ MAX_S3_FILE_SIZE = 5368709120
5
+
6
+ protected
7
+
8
+ def active?
9
+ bucket && key && secret
10
+ end
11
+
12
+ def path
13
+ @path ||= expand(config[:s3, :path] || config[:local, :path] || ":kind/:id")
14
+ end
15
+
16
+ def save
17
+ # FIXME: user friendly error here :)
18
+ raise RuntimeError, "pipe-streaming not supported for S3." unless @backup.path
19
+
20
+ # needed in cleanup even on dry run
21
+ AWS::S3::Base.establish_connection!(:access_key_id => key, :secret_access_key => secret, :use_ssl => true) unless $LOCAL
22
+
23
+ unless $DRY_RUN || $LOCAL
24
+ benchmark = Benchmark.realtime do
25
+ AWS::S3::Bucket.create(bucket)
26
+ store_files_to_bucket(@backup.path)
27
+ end
28
+ puts("Upload took " + sprintf("%.2f", benchmark) + " second(s).") if $_VERBOSE
29
+ end
30
+ end
31
+
32
+ def cleanup
33
+ return if $LOCAL
34
+
35
+ return unless keep = @config[:keep, :s3]
36
+
37
+ puts "listing files: #{bucket}:#{base}*" if $_VERBOSE
38
+ files = AWS::S3::Bucket.objects(bucket, :prefix => base, :max_keys => keep * 2)
39
+ puts files.collect {|x| x.key} if $_VERBOSE
40
+
41
+ files = files.collect {|x| x.key}.sort
42
+
43
+ cleanup_with_limit(files, keep) do |f|
44
+ puts "removing s3 file #{bucket}:#{f}" if $DRY_RUN || $_VERBOSE
45
+ AWS::S3::Bucket.find(bucket)[f].delete unless $DRY_RUN || $LOCAL
46
+ end
47
+ end
48
+
49
+ def bucket
50
+ @config[:s3, :bucket]
51
+ end
52
+
53
+ def key
54
+ @config[:s3, :key]
55
+ end
56
+
57
+ def secret
58
+ @config[:s3, :secret]
59
+ end
60
+
61
+ def store_files_to_bucket(files)
62
+ if files.class == String
63
+ store_file(files)
64
+ elsif files.class == Array
65
+ files.each do |file|
66
+ store_file(file)
67
+ end
68
+ end
69
+ end
70
+
71
+ def store_file(filepath)
72
+ if File.stat(filepath).size > MAX_S3_FILE_SIZE
73
+ STDERR.puts "ERROR: File size exceeds maximum allowed for upload to S3 (#{MAX_S3_FILE_SIZE}): #{@backup.path}"
74
+ return
75
+ end
76
+ File.open(filepath) do |file|
77
+ destination_path = full_path(File.basename(filepath), '')
78
+ puts "Uploading #{bucket}:#{destination_path}" if $_VERBOSE
79
+ AWS::S3::S3Object.store(destination_path, file, bucket)
80
+ puts "...done" if $_VERBOSE
81
+ end
82
+ end
83
+
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,88 @@
1
+ module Astrails
2
+ module Safe
3
+ class Sftp < Sink
4
+
5
+ protected
6
+
7
+ def active?
8
+ host && user
9
+ end
10
+
11
+ def path
12
+ @path ||= expand(config[:sftp, :path] || config[:local, :path] || ":kind/:id")
13
+ end
14
+
15
+ def save
16
+ raise RuntimeError, "pipe-streaming not supported for SFTP." unless @backup.path
17
+
18
+ puts "Uploading #{host}:#{full_path} via SFTP" if $_VERBOSE || $DRY_RUN
19
+
20
+ unless $DRY_RUN || $LOCAL
21
+ opts = {}
22
+ opts[:password] = password if password
23
+ opts[:port] = port if port
24
+ Net::SFTP.start(host, user, opts) do |sftp|
25
+ puts "Sending #{@backup.path} to #{full_path}" if $_VERBOSE
26
+ begin
27
+ sftp.upload! @backup.path, full_path
28
+ rescue Net::SFTP::StatusException
29
+ puts "Ensuring remote path (#{path}) exists" if $_VERBOSE
30
+ # mkdir -p
31
+ folders = path.split('/')
32
+ folders.each_index do |i|
33
+ folder = folders[0..i].join('/')
34
+ puts "Creating #{folder} on remote" if $_VERBOSE
35
+ sftp.mkdir!(folder) rescue Net::SFTP::StatusException
36
+ end
37
+ retry
38
+ end
39
+ end
40
+ puts "...done" if $_VERBOSE
41
+ end
42
+ end
43
+
44
+ def cleanup
45
+ return if $LOCAL || $DRY_RUN
46
+
47
+ return unless keep = @config[:keep, :sftp]
48
+
49
+ puts "listing files: #{host}:#{base}*" if $_VERBOSE
50
+ opts = {}
51
+ opts[:password] = password if password
52
+ opts[:port] = port if port
53
+ Net::SFTP.start(host, user, opts) do |sftp|
54
+ files = sftp.dir.glob(path, File.basename("#{base}*"))
55
+
56
+ puts files.collect {|x| x.name } if $_VERBOSE
57
+
58
+ files = files.
59
+ collect {|x| x.name }.
60
+ sort
61
+
62
+ cleanup_with_limit(files, keep) do |f|
63
+ file = File.join(path, f)
64
+ puts "removing sftp file #{host}:#{file}" if $DRY_RUN || $_VERBOSE
65
+ sftp.remove!(file) unless $DRY_RUN || $LOCAL
66
+ end
67
+ end
68
+ end
69
+
70
+ def host
71
+ @config[:sftp, :host]
72
+ end
73
+
74
+ def user
75
+ @config[:sftp, :user]
76
+ end
77
+
78
+ def password
79
+ @config[:sftp, :password]
80
+ end
81
+
82
+ def port
83
+ @config[:sftp, :port]
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,35 @@
1
+ module Astrails
2
+ module Safe
3
+ class Sink < Stream
4
+
5
+ def process
6
+ return unless active?
7
+
8
+ save
9
+ cleanup
10
+ end
11
+
12
+ protected
13
+
14
+ # path is defined in subclass
15
+ # base is used in 'cleanup' to find all files that begin with base. the '.'
16
+ # at the end is essential to distinguish b/w foo.* and foobar.* archives for example
17
+ def base
18
+ @base ||= File.join(path, File.basename(@backup.filename).split(".").first + '.')
19
+ end
20
+
21
+ def full_path(filename = @backup.filename, extension = @backup.extension)
22
+ File.join(path, filename) + extension
23
+ end
24
+
25
+ # call block on files to be removed (all except for the LAST 'limit' files
26
+ def cleanup_with_limit(files, limit, &block)
27
+ return unless files.size > limit
28
+
29
+ to_remove = files[0..(files.size - limit - 1)]
30
+ # TODO: validate here
31
+ to_remove.each(&block)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ module Astrails
2
+ module Safe
3
+ class Source < Stream
4
+
5
+ attr_accessor :id
6
+ def initialize(id, config)
7
+ @id, @config = id.to_s, config
8
+ end
9
+
10
+ def timestamp
11
+ Time.now.strftime("%y%m%d-%H%M")
12
+ end
13
+
14
+ def kind
15
+ self.class.human_name
16
+ end
17
+
18
+ def filename
19
+ @filename ||= expand(":kind-:id.:timestamp")
20
+ end
21
+
22
+ def backup
23
+ return @backup if @backup
24
+ @backup = Backup.new(
25
+ :id => @id,
26
+ :kind => kind,
27
+ :extension => extension,
28
+ :command => command,
29
+ :timestamp => timestamp
30
+ )
31
+ # can't do this in the initializer hash above since
32
+ # filename() calls expand() which requires @backup
33
+ # FIXME: move expansion to the backup (last step in ctor) assign :tags here
34
+ @backup.filename = filename
35
+ @backup
36
+ end
37
+
38
+ protected
39
+
40
+ def self.human_name
41
+ name.split('::').last.downcase
42
+ end
43
+
44
+ end
45
+ end
46
+ end
47
+
@@ -0,0 +1,20 @@
1
+ module Astrails
2
+ module Safe
3
+ class Stream
4
+
5
+ attr_accessor :config, :backup
6
+ def initialize(config, backup)
7
+ @config, @backup = config, backup
8
+ end
9
+
10
+ # FIXME: move to Backup
11
+ def expand(path)
12
+ path .
13
+ gsub(/:kind\b/, @backup.kind.to_s) .
14
+ gsub(/:id\b/, @backup.id.to_s) .
15
+ gsub(/:timestamp\b/, @backup.timestamp)
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module Astrails
2
+ module Safe
3
+ class Svndump < Source
4
+
5
+ def command
6
+ "svnadmin dump #{@config[:options]} #{@config[:repo_path]}"
7
+ end
8
+
9
+ def extension; '.svn'; end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ require 'tmpdir'
2
+ module Astrails
3
+ module Safe
4
+ module TmpFile
5
+ @keep_files = []
6
+
7
+ def self.tmproot
8
+ @tmproot ||= Dir.mktmpdir
9
+ end
10
+
11
+ def self.cleanup
12
+ begin
13
+ FileUtils.remove_entry_secure tmproot
14
+ rescue ArgumentError => e
15
+ if e.message =~ /parent directory is world writable/
16
+ puts <<-ERR
17
+
18
+
19
+ ********************************************************************************
20
+ It looks like you have wrong permissions on your TEMP directory. The usual
21
+ case is when you have world writable TEMP directory withOUT the sticky bit.
22
+
23
+ Try "chmod +t" on it.
24
+
25
+ ********************************************************************************
26
+
27
+ ERR
28
+ else
29
+ raise
30
+ end
31
+ end
32
+ @tmproot = nil
33
+ end
34
+
35
+ def self.create(name)
36
+ # create temp directory
37
+
38
+ file = Tempfile.new(name, tmproot)
39
+
40
+ yield file
41
+
42
+ file.close
43
+ @keep_files << file # so that it will not get gcollected and removed from filesystem until the end
44
+ file.path
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ require 'tmpdir'
2
+
3
+ unless Dir.respond_to?(:mktmpdir)
4
+ # backward compat for 1.8.6
5
+ class Dir
6
+ def Dir.mktmpdir(prefix_suffix=nil, tmpdir=nil)
7
+ case prefix_suffix
8
+ when nil
9
+ prefix = "d"
10
+ suffix = ""
11
+ when String
12
+ prefix = prefix_suffix
13
+ suffix = ""
14
+ when Array
15
+ prefix = prefix_suffix[0]
16
+ suffix = prefix_suffix[1]
17
+ else
18
+ raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
19
+ end
20
+ tmpdir ||= Dir.tmpdir
21
+ t = Time.now.strftime("%Y%m%d")
22
+ n = nil
23
+ begin
24
+ path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
25
+ path << "-#{n}" if n
26
+ path << suffix
27
+ Dir.mkdir(path, 0700)
28
+ rescue Errno::EEXIST
29
+ n ||= 0
30
+ n += 1
31
+ retry
32
+ end
33
+
34
+ if block_given?
35
+ begin
36
+ yield path
37
+ ensure
38
+ FileUtils.remove_entry_secure path
39
+ end
40
+ else
41
+ path
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,88 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../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
+ Astrails::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
+
50
+ @backup = "#{dst}/archive/archive-test1.#{@timestamp}.tar.gz"
51
+ end
52
+
53
+ it "should create backup file" do
54
+ File.exists?(@backup).should be_true
55
+ end
56
+
57
+ describe "after extracting" do
58
+ before(:all) do
59
+ # prepare target dir
60
+ @target = "#{@root}/test"
61
+ mkdir_p @target
62
+ system "tar -zxvf #{@backup} -C #{@target}"
63
+
64
+ @test = "#{@target}/#{@root}/src"
65
+ puts @test
66
+ end
67
+
68
+ it "should include asd1/2/3" do
69
+ File.exists?("#{@test}/asd1").should be_true
70
+ File.exists?("#{@test}/a/asd2").should be_true
71
+ File.exists?("#{@test}/a/s/asd3").should be_true
72
+ end
73
+
74
+ it "should only include qwe 1 and 2 (no 3)" do
75
+ File.exists?("#{@test}/qwe1").should be_true
76
+ File.exists?("#{@test}/q/qwe2").should be_true
77
+ File.exists?("#{@test}/q/w/qwe3").should be_false
78
+ File.exists?("#{@test}/q/w/e/qwe4").should be_false
79
+ end
80
+
81
+ it "should preserve file content" do
82
+ File.read("#{@test}/qwe1").should == "qwe"
83
+ File.read("#{@test}/q/qwe2").should == "qweqwe"
84
+ File.read("#{@test}/a/s/asd3").should == "asdasdasd"
85
+ end
86
+ end
87
+
88
+ end