ruby_clone 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.
@@ -0,0 +1,94 @@
1
+ require 'pty'
2
+
3
+ module RubyClone
4
+
5
+ class RSync
6
+
7
+ attr_writer :dry_run
8
+
9
+ def initialize(output)
10
+ @configurations = { options: '-Cavh --stats --progress', show_command: true, show_output: true }
11
+ @exclude_patterns = []
12
+ @include_patterns = []
13
+ @output = output
14
+ @profiles = {}
15
+ end
16
+
17
+ def profiles=(profile)
18
+ @profiles[profile.name] = profile
19
+ @last_profile = profile
20
+ end
21
+
22
+ def last_profile
23
+ @last_profile
24
+ end
25
+
26
+ def update_configurations(configurations)
27
+ @configurations.merge! configurations
28
+ end
29
+
30
+ def exclude_pattern=(path)
31
+ @exclude_patterns << path
32
+ end
33
+
34
+ def include_pattern=(path)
35
+ @include_patterns << path
36
+ end
37
+
38
+ def rsync_command(profile_name)
39
+ profile = @profiles[profile_name.to_s]
40
+
41
+ if profile
42
+ raise SyntaxError, "Empty Profile not allowed for profile #{profile} with no 'from folder'" unless profile.from_folder
43
+ raise SyntaxError, "Empty Profile not allowed for profile #{profile} with no 'to folder'" unless profile.to_folder
44
+
45
+ create_rsync_command profile
46
+ else
47
+ raise ArgumentError, "Profile #{profile_name} not found"
48
+ end
49
+ end
50
+
51
+ def run(profile_name)
52
+ command = rsync_command(profile_name)
53
+ @output.puts "\n#{command}\n\n" if @configurations[:show_command]
54
+
55
+ pty = @pty || PTY
56
+
57
+ pty.spawn(command) do |r, w, pid|
58
+ r.each { |line| print line } if @configurations[:show_output]
59
+ end
60
+
61
+ profile = @profiles[profile_name.to_s]
62
+ profile.to_folder.delete_files
63
+ end
64
+
65
+ private
66
+
67
+ def create_rsync_command(profile)
68
+ command = "rsync #{@configurations[:options]} "
69
+
70
+ command << "-n " if @dry_run
71
+
72
+ command << @include_patterns.map { |e| "--include=#{e}" }.join(" ") + " "
73
+ command << @exclude_patterns.map { |e| "--exclude=#{e}" }.join(" ")
74
+
75
+ command << " #{profile.from_folder.to_command}"
76
+ command << " #{profile.to_folder.to_command}"
77
+ command << " #{create_ssh_command(profile)}"
78
+
79
+ command << " #{profile.from_folder} #{profile.to_folder}"
80
+ command.gsub(/\s+/, " ")
81
+ end
82
+
83
+ def create_ssh_command(profile)
84
+ from_folder = profile.from_folder
85
+ to_folder = profile.to_folder
86
+
87
+ if from_folder.ssh? && to_folder.ssh?
88
+ raise SyntaxError, 'The source and destination cannot both be remote.'
89
+ elsif from_folder.ssh? || to_folder.ssh?
90
+ "-e ssh"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,3 @@
1
+ module RubyClone
2
+ VERSION = "1.0.0"
3
+ end
data/lib/ruby_clone.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "ruby_clone/version"
2
+
3
+ require 'ruby_clone/backup_utils'
4
+
5
+ require 'ruby_clone/rsync'
6
+
7
+ require 'ruby_clone/dsl'
8
+
9
+ require 'ruby_clone/profile'
10
+
11
+ require 'ruby_clone/options'
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ruby_clone/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Frederico Benevides"]
6
+ gem.email = ["fredbene@gmail.com"]
7
+ gem.description = %q{Ruby clone is a command line tool to work with Rsync using DSL!}
8
+ gem.summary = %q{Ruby_clone is high level script of Rsync. Use Ruby DSL to work with RSync! }
9
+ gem.homepage = "http://github.com/fredericobenevides/ruby_clone"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "ruby_clone"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = RubyClone::VERSION
17
+
18
+ gem.add_development_dependency "guard", "~> 1.3.2"
19
+ gem.add_development_dependency "guard-rspec", "~> 1.2.1"
20
+ gem.add_development_dependency "rspec", "~> 2.11.0"
21
+ end
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ module RubyClone
4
+
5
+ describe BackupUtils do
6
+
7
+ describe "Deleting files" do
8
+
9
+ before(:each) do
10
+ @ruby_clone_suffix = '_rbcl'
11
+
12
+ @paths = [ "/my_path/sub_path1/file1#{@ruby_clone_suffix}_20120921",
13
+ "/my_path/sub_path1/file1#{@ruby_clone_suffix}_20120922",
14
+ "/my_path/sub_path1/file1#{@ruby_clone_suffix}_20120923",
15
+
16
+ "/my_path/sub_path1/file2#{@ruby_clone_suffix}_20120922",
17
+ "/my_path/sub_path2/file1#{@ruby_clone_suffix}_20120922",
18
+ "/my_path/sub_path2/file2#{@ruby_clone_suffix}_20120923"]
19
+
20
+ File.stub(:exists?).with(any_args()).and_return(true)
21
+
22
+ FileTest.stub(:file?).with(any_args()).and_return false
23
+ FileTest.stub(:file?).with(@paths[0]).and_return true
24
+ FileTest.stub(:file?).with(@paths[1]).and_return true
25
+ FileTest.stub(:file?).with(@paths[2]).and_return true
26
+ FileTest.stub(:file?).with(@paths[3]).and_return true
27
+ FileTest.stub(:file?).with(@paths[4]).and_return true
28
+ FileTest.stub(:file?).with(@paths[5]).and_return true
29
+
30
+ Find.stub(:find).and_yield('/my_path').and_yield('/my_path/sub_path1')
31
+ .and_yield(@paths[0]).and_yield(@paths[1]).and_yield(@paths[2]).and_yield(@paths[3])
32
+ .and_yield('/my_path/sub_path2')
33
+ .and_yield(@paths[4]).and_yield(@paths[5])
34
+
35
+ end
36
+
37
+ describe "valid" do
38
+
39
+ it "should create a hash with [path_files_without_suffix, path_files] that points to the files with same name" do
40
+ paths = BackupUtils.find_files_with_same_name '/my_path', @ruby_clone_suffix
41
+
42
+ paths.should == { '/my_path/sub_path1/file1' => [@paths[0], @paths[1], @paths[2]],
43
+ '/my_path/sub_path1/file2' => [@paths[3]],
44
+ '/my_path/sub_path2/file1' => [@paths[4]],
45
+ '/my_path/sub_path2/file2' => [@paths[5]] }
46
+ end
47
+
48
+ it "should delete old files with same name but different 'suffix' when passed the limit" do
49
+ FileUtils.should_receive(:remove_entry).with(@paths[0])
50
+ FileUtils.should_receive(:remove_entry).with(@paths[1])
51
+ FileUtils.should_not_receive(:remove_entry).with(@paths[2])
52
+
53
+ BackupUtils.delete_files('/my_path', @ruby_clone_suffix, 1)
54
+ end
55
+
56
+ end
57
+
58
+ describe "invalid" do
59
+
60
+ it "should not delete files with path that doesn't exist" do
61
+ File.stub(:exists?).with(any_args()).and_return(false)
62
+ Find.should_not_receive(:find)
63
+
64
+ BackupUtils.delete_files('/my_path', @ruby_clone_suffix, 5)
65
+ end
66
+
67
+ it "should not delete files with same name but different 'suffix' when it didn't pass the limit" do
68
+ FileUtils.should_not_receive(:remove_entry)
69
+ BackupUtils.delete_files('/my_path', @ruby_clone_suffix, 5)
70
+ end
71
+
72
+ it "should not delete files with same name but different 'suffix' when the found files is on the limit" do
73
+ FileUtils.should_not_receive(:remove_entry)
74
+ BackupUtils.delete_files('/my_path', @ruby_clone_suffix, 3)
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,259 @@
1
+ require 'spec_helper'
2
+
3
+ module RubyClone
4
+
5
+ describe "DSL" do
6
+
7
+ before(:all) do
8
+ class DummyClass
9
+ end
10
+ DummyClass.extend RubyClone::DSL
11
+
12
+ @rsync_options = '-Cavh --stats --progress'
13
+ @rsync_command = "rsync #{@rsync_options}"
14
+ @folders = "/from_folder /to_folder"
15
+ end
16
+
17
+ before(:each) do
18
+ @rsync = DummyClass.rsync_new_instance
19
+ end
20
+
21
+ describe "Creating a Profile" do
22
+
23
+ describe "valid" do
24
+
25
+ it "should create the following command 'rsync -Cav --stats /from_folder /to_folder'" do
26
+ DummyClass.profile('backup1') do
27
+ DummyClass.from('/from_folder')
28
+ DummyClass.to('/to_folder')
29
+ end
30
+
31
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} #{@folders}"
32
+ end
33
+
34
+ end
35
+
36
+ describe "invalid" do
37
+
38
+ it "should raise SyntaxError with message 'Empty Profile not allowed' for profile with no block" do
39
+ lambda do
40
+ DummyClass.profile('backup')
41
+ end.should raise_error(SyntaxError, 'Empty Profile not allowed')
42
+ end
43
+
44
+ it "should raise SyntaxError with message 'Empty Profile not allowed' for empty block" do
45
+ lambda do
46
+ DummyClass.profile('backup') { }
47
+ end.should raise_error(SyntaxError, 'Empty Profile not allowed')
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#config" do
53
+
54
+ it "should as default have a default configurations" do
55
+
56
+ DummyClass.profile('backup1') do
57
+ DummyClass.from('/from_folder')
58
+ DummyClass.to('/to_folder')
59
+ end
60
+
61
+ @rsync.instance_eval { @configurations }.should == { options: @rsync_options, show_command: true, show_output: true }
62
+ end
63
+
64
+ it "should change the default configurations when the config has new values" do
65
+ DummyClass.config options: '-Cav', show_command: false, show_output: false
66
+
67
+ DummyClass.profile('backup1') do
68
+ DummyClass.from('/from_folder')
69
+ DummyClass.to('/to_folder')
70
+ end
71
+
72
+ @rsync.instance_eval { @configurations }.should == { options: '-Cav', show_command: false, show_output: false }
73
+ end
74
+ end
75
+
76
+ describe "#from" do
77
+
78
+ it "should create exclude paths just for the profile 'backup1' if exclude DSL is not on the top" do
79
+ DummyClass.profile('backup1') do
80
+ DummyClass.from('/from_folder') do
81
+ DummyClass.exclude_pattern '/exclude_pattern1'
82
+ end
83
+ DummyClass.to('/to_folder')
84
+ end
85
+
86
+ DummyClass.profile('backup2') do
87
+ DummyClass.from('/from_folder')
88
+ DummyClass.to('/to_folder')
89
+ end
90
+
91
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --exclude=/exclude_pattern1 #{@folders}"
92
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} #{@folders}"
93
+ end
94
+
95
+ it "should create include just for the profile 'backup1' if include_pattern DSL is set up only in the profile" do
96
+ DummyClass.profile('backup1') do
97
+ DummyClass.from('/from_folder') do
98
+ DummyClass.include_pattern '/include_pattern1'
99
+ end
100
+ DummyClass.to('/to_folder')
101
+ end
102
+
103
+ DummyClass.profile('backup2') do
104
+ DummyClass.from('/from_folder')
105
+ DummyClass.to('/to_folder')
106
+ end
107
+
108
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --include=/include_pattern1 #{@folders}"
109
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} #{@folders}"
110
+ end
111
+
112
+ it "should have the following options '-e ssh user@server:/from_folder /to_folder' when is ssh" do
113
+ DummyClass.profile('backup1') do
114
+ DummyClass.from('/from_folder', ssh: "user@server")
115
+ DummyClass.to('/to_folder')
116
+ end
117
+
118
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} -e ssh user@server:/from_folder /to_folder"
119
+ end
120
+ end
121
+
122
+ describe "#to" do
123
+
124
+ it "should have '--delete' when options 'delete' is set up" do
125
+ DummyClass.profile('backup1') do
126
+ DummyClass.from('/from_folder')
127
+ DummyClass.to('/to_folder', delete: true)
128
+ end
129
+
130
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --delete #{@folders}"
131
+ end
132
+
133
+ it "should have '--delete-excluded' when options 'delete_excluded' is set up" do
134
+ DummyClass.profile('backup1') do
135
+ DummyClass.from('/from_folder')
136
+ DummyClass.to('/to_folder', delete_excluded: true)
137
+ end
138
+
139
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --delete-excluded #{@folders}"
140
+ end
141
+ end
142
+
143
+ describe "#exclude" do
144
+
145
+ it "should create exclude paths for all profiles if exclude DSL is on the top" do
146
+ DummyClass.exclude_pattern '/exclude_top_path'
147
+
148
+ DummyClass.profile('backup1') do
149
+ DummyClass.from('/from_folder')
150
+ DummyClass.to('/to_folder')
151
+ end
152
+
153
+ DummyClass.profile('backup2') do
154
+ DummyClass.from('/from_folder')
155
+ DummyClass.to('/to_folder')
156
+ end
157
+
158
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --exclude=/exclude_top_path #{@folders}"
159
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} --exclude=/exclude_top_path #{@folders}"
160
+ end
161
+
162
+ it "should include the exclude path from the top and profile if exclude DSL are set up in the top and in profile" do
163
+ DummyClass.exclude_pattern 'exclude_top_path'
164
+
165
+ DummyClass.profile('backup1') do
166
+ DummyClass.from('/from_folder') do
167
+ DummyClass.exclude_pattern'/exclude_pattern1'
168
+ end
169
+ DummyClass.to('/to_folder')
170
+ end
171
+
172
+ DummyClass.profile('backup2') do
173
+ DummyClass.from('/from_folder')
174
+ DummyClass.to('/to_folder')
175
+ end
176
+
177
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --exclude=exclude_top_path --exclude=/exclude_pattern1 #{@folders}"
178
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} --exclude=exclude_top_path #{@folders}"
179
+ end
180
+
181
+ end
182
+
183
+ describe "#include_pattern" do
184
+
185
+ it "should create include for all profiles if include_pattern DSL is on the top" do
186
+ DummyClass.include_pattern '/include_top_path'
187
+
188
+ DummyClass.profile('backup1') do
189
+ DummyClass.from('/from_folder')
190
+ DummyClass.to('/to_folder')
191
+ end
192
+
193
+ DummyClass.profile('backup2') do
194
+ DummyClass.from('/from_folder')
195
+ DummyClass.to('/to_folder')
196
+ end
197
+
198
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --include=/include_top_path #{@folders}"
199
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} --include=/include_top_path #{@folders}"
200
+ end
201
+
202
+ it "should create the include on the top and in profile if include_pattern DSL are set up in the top and in profile" do
203
+ DummyClass.include_pattern '/include_top_path'
204
+
205
+ DummyClass.profile('backup1') do
206
+ DummyClass.from('/from_folder') do
207
+ DummyClass.include_pattern '/include_pattern1'
208
+ end
209
+ DummyClass.to('/to_folder')
210
+ end
211
+
212
+ DummyClass.profile('backup2') do
213
+ DummyClass.from('/from_folder')
214
+ DummyClass.to('/to_folder')
215
+ end
216
+
217
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --include=/include_top_path --include=/include_pattern1 #{@folders}"
218
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} --include=/include_top_path #{@folders}"
219
+ end
220
+
221
+ end
222
+
223
+ describe "#backup" do
224
+
225
+ it "should include the backup commands when the backup is set up" do
226
+ DummyClass.profile('backup1') do
227
+ DummyClass.from('/from_folder')
228
+ DummyClass.to('/to_folder') do
229
+ DummyClass.backup('/backup_folder')
230
+ end
231
+ end
232
+
233
+ @rsync.rsync_command('backup1').should =~ /#{@rsync_command} -b --suffix=_rbcl_\d{8} --backup-dir=\/backup_folder #{@folders}/
234
+ end
235
+
236
+ it "should include the suffix when the option suffix: is set up" do
237
+ DummyClass.profile('backup1') do
238
+ DummyClass.from('/from_folder')
239
+ DummyClass.to('/to_folder') do
240
+ DummyClass.backup('/backup_folder', suffix: '_my_suffix')
241
+ end
242
+ end
243
+
244
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} -b --suffix=_rbcl_my_suffix --backup-dir=/backup_folder #{@folders}"
245
+ end
246
+
247
+ it "should not include any suffix if the option disable_suffix is true" do
248
+ DummyClass.profile('backup1') do
249
+ DummyClass.from('/from_folder')
250
+ DummyClass.to('/to_folder') do
251
+ DummyClass.backup('/backup_folder', suffix: '_my_suffix', disable_suffix: true)
252
+ end
253
+ end
254
+
255
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} -b --backup-dir=/backup_folder #{@folders}"
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+
4
+ module RubyClone
5
+
6
+ describe Options do
7
+
8
+ before(:each) do
9
+ @output = StringIO.new
10
+ @options = Options.new(@output)
11
+ end
12
+
13
+ describe "Help messages" do
14
+
15
+ it "should show if ARGV is empty" do
16
+ opts = @options.parse([])
17
+ opts.on.to_s.should =~ /-h, --help\s+This message.*/
18
+ end
19
+
20
+ it "should show if ARGV have the option '-h'" do
21
+ @options.parse(%w[-h])
22
+
23
+ @output.seek 0
24
+ @output.read.should =~ /-h, --help\s+This message.*/
25
+ end
26
+
27
+ it "should not show if ARGV have another option that exists and is not '-h'" do
28
+ @options.parse(%w[-b /my_file])
29
+
30
+ @output.seek 0
31
+ @output.read.should_not =~ /-h, --help\s+This message.*/
32
+ end
33
+
34
+ it "should have the banner with message 'Usage: ruby_clone [options] profile'" do
35
+ opts = @options.parse([])
36
+ opts.banner.should == 'Usage: ruby_clone [options] profile'
37
+ end
38
+ end
39
+
40
+ describe "Backup file" do
41
+
42
+ it "should as default the backup file path be '~/.ruby_clone'" do
43
+ @options.parse(%w[backup_profile])
44
+ @options.configurations[:backup_file].should == '~/.ruby_clone'
45
+ end
46
+
47
+ it "should set a new backup file location with the options is '-b'" do
48
+ @options.parse(%w[-b /my_file backup_profile])
49
+ @options.configurations[:backup_file].should == '/my_file'
50
+ end
51
+ end
52
+
53
+ it "should set dry-run mode when the option is '-d'" do
54
+ @options.parse(%w[-d])
55
+ @options.configurations[:dry_run].should be_true
56
+ end
57
+
58
+ end
59
+
60
+ end