monster_remote 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,84 +4,140 @@ module Monster
4
4
  module Remote
5
5
  module Wrappers
6
6
 
7
- class MonsterRemoteNetFTPWrapper < StandardError; end
8
- class NetFTPPermissionDenied < MonsterRemoteNetFTPWrapper; end
9
-
10
7
  class NetFTP
11
8
 
12
- def initialize(provider = Net::FTP)
13
- @provider = provider
14
- @filters = []
15
- @nslt = {}
9
+ def initialize(driver=Net::FTP)
10
+ @driver = driver
16
11
  end
17
12
 
18
- def open(host, port, user, pass)
19
- @provider.open(host) do |ftp|
20
- @ftp = ftp
21
- @ftp.connect(host, port)
22
- @ftp.login(user, pass)
23
- yield(self, ftp)
13
+ def open(host, user, password, port, &block)
14
+ ftp = @driver.new
15
+ ftp.connect(host, port)
16
+ ftp.login(user, password)
17
+ if block
18
+ block.call(NetFTPHandler.new(ftp), ftp)
24
19
  end
20
+ ftp.close
25
21
  end
26
22
 
27
- def copy_dir(local_dir, remote_dir)
28
- create_if_not_exists(remote_dir)
29
- local_structure = filter(Dir.entries(local_dir))
30
- local_structure.each do |entry|
31
- local_path = File.join(local_dir, entry)
32
- remote_path = File.join(remote_dir, entry)
33
- copy(local_path, remote_path)
23
+ end# NetFTP
24
+
25
+ class NetFTPHandler
26
+
27
+ def initialize(ftp)
28
+ @ftp = ftp
29
+ end
30
+
31
+ def create_dir(dir)
32
+ pwd = @ftp.pwd
33
+
34
+ dirs = dirs_in_path(dir)
35
+ root_dir_name = dirs.shift
36
+
37
+ create_and_chdir(root_dir_name)
38
+
39
+ if dirs.size > 0
40
+ dirs.each do |dir|
41
+ create_and_chdir(dir)
42
+ end
34
43
  end
44
+
45
+ @ftp.chdir(pwd)
35
46
  end
36
47
 
37
- def add_filter(filter)
38
- @filters ||= []
39
- @filters << filter
48
+ def remove_dir(dir)
49
+ pwd = @ftp.pwd
50
+ dirs = dirs_in_path(dir)
51
+ final_dir = dirs.pop
52
+ dirs.each { |dir| @ftp.chdir(dir) }
53
+ empty_and_remove_dir(final_dir)
54
+ while(final_dir = dirs.pop)
55
+ @ftp.chdir("..")
56
+ empty_and_remove_dir(final_dir)
57
+ end
58
+ @ftp.chdir(pwd)
40
59
  end
41
60
 
42
- private
43
- def copy(from, to)
44
- if(Dir.exists?(from))
45
- copy_dir(from, to)
61
+ def copy_file(from, to)
62
+ if (dirs = dirs_in_path(to)).size > 1
63
+ file = dirs.pop
64
+ create_dir(dirs.join("/"))
65
+
66
+ pwd = @ftp.pwd
67
+ dirs.each { |dir| @ftp.chdir(dir) }
68
+ @ftp.putbinaryfile(from, file)
69
+ @ftp.chdir(pwd)
46
70
  else
47
- copy_file(from, to)
71
+ @ftp.putbinaryfile(from, to)
48
72
  end
49
73
  end
50
74
 
51
- def copy_file(from, to)
52
- @ftp.putbinaryfile(from, to)
75
+ def remove_file(file)
76
+ @ftp.delete(file)
53
77
  end
54
78
 
55
- def filter(dir_structure)
56
- if(@filters.empty?)
57
- @filters << ContentNameBasedFilter.new.reject([".", ".."])
79
+ private
80
+
81
+ def entry_from_regex_on_path(regex, path)
82
+ (matcher = regex.match(path)) && matcher[2].strip
83
+ end
84
+
85
+ def empty_dir(dir)
86
+ pwd = @ftp.pwd
87
+ @ftp.chdir(dir)
88
+ res = @ftp.list;
89
+ res.shift
90
+
91
+ res.each do |item|
92
+ if dir = entry_from_regex_on_path(/(^d.*)(\s.*)/i, item)
93
+ remove_dir(dir)
94
+ end
95
+
96
+ if file = entry_from_regex_on_path(/(^-.*)(\s.*)/i, item)
97
+ remove_file(file)
98
+ end
58
99
  end
59
100
 
60
- allowed = dir_structure
61
- @filters.each do |f|
62
- allowed = f.filter(allowed)
101
+ @ftp.chdir(pwd)
102
+ end
103
+
104
+ def empty_and_remove_dir(dir)
105
+ empty_dir(dir)
106
+ if(@ftp.nlst.include?(dir))
107
+ @ftp.rmdir(dir)
63
108
  end
64
- allowed
65
109
  end
66
110
 
67
- def create_remote_dir(dir)
68
- dirname = File.dirname(dir)
69
- dir_content = @nslt[dirname] || @ftp.nlst(dirname)
70
- dir_exists = dir_content.include? dir
71
- @ftp.mkdir(dir) unless dir_exists
111
+ def create_and_chdir(dir)
112
+ create_dir_if_not_exists(dir)
113
+ @ftp.chdir(dir)
114
+ end
115
+
116
+ def dirs_in_path(dir)
117
+ dirs_in_path = dir.gsub(/\.*\/$/, "").split("/")
72
118
  end
73
119
 
74
- def create_if_not_exists(dir)
120
+ def is_new_dir?(dir)
121
+ is_new_dir = true
75
122
  begin
76
- create_remote_dir(dir)
77
- rescue Net::FTPPermError => e
78
- denied = NetFTPPermissionDenied.new(e)
79
- raise denied, e.message
123
+ is_new_dir = @ftp.nlst(dir).empty?
124
+ rescue Net::FTPTempError => e
125
+ is_unexpected_error = !e.message.include?("450")
126
+ if is_unexpected_error
127
+ raise(e, e.message, caller)
128
+ end
129
+ is_new_dir = !e.message.include?("No files found")
80
130
  end
131
+ return is_new_dir
81
132
  end
82
133
 
83
- end # NetFTP
134
+ def create_dir_if_not_exists(dir)
135
+ if is_new_dir?(dir)
136
+ @ftp.mkdir(dir)
137
+ end
138
+ end
139
+ end# NetFTPHandler
84
140
 
85
- end
86
- end
87
- end
141
+ end# Wrappers
142
+ end# Remote
143
+ end# Monster
@@ -1,3 +1,15 @@
1
- require 'monster/remote/sync'
2
1
  require 'monster/remote/wrappers/net_ftp'
3
- require 'monster/remote/content_name_based_filter'
2
+ require 'monster/remote/filters/filter'
3
+ require 'monster/remote/filters/name_based_filter'
4
+ require 'monster/remote/configuration'
5
+ require 'monster/remote/sync'
6
+ require 'monster/remote/cli'
7
+
8
+ module Monster
9
+ module Remote
10
+
11
+ class MissingProtocolWrapperError < StandardError; end
12
+ class MissingLocalDirError < StandardError; end
13
+ class MissingRemoteDirError < StandardError; end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ module Monster
2
+ module Remote
3
+ VERSION = "0.1.0"
4
+ NAME = File.basename(__FILE__)
5
+ end
6
+ end
7
+
8
+ require 'monster/remote'
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ lib = File.expand_path('../lib/', __FILE__)
4
+ $:.unshift lib unless $:.include?(lib)
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "monster_remote"
8
+ s.version = "0.1.0"
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["Ricardo Valeriano"]
11
+ s.email = ["ricardo.valeriano@gmail.com"]
12
+ s.homepage = "http://github.com/ricardovaleriano/monster_remote"
13
+ s.summary = "Publish your jekyll blog via ftp easy as pie"
14
+ s.description = "This gem allow you publish your jekyll static site via FTP, easy as pie."
15
+
16
+ s.required_rubygems_version = ">= 0"
17
+
18
+ s.add_development_dependency "rspec"
19
+ s.add_development_dependency "fakefs"
20
+
21
+ s.require_path = 'lib'
22
+
23
+ s.files = ["Gemfile", "LICENSE", "README.md", "Rakefile", "bin/monster_remote", "lib/monster/remote.rb", "lib/monster/remote/cli.rb", "lib/monster/remote/configuration.rb", "lib/monster/remote/filters/filter.rb", "lib/monster/remote/filters/name_based_filter.rb", "lib/monster/remote/sync.rb", "lib/monster/remote/wrappers/net_ftp.rb", "lib/monster_remote.rb", "monster_remote.gemspec", "spec/monster/remote/cli_spec.rb", "spec/monster/remote/configuration_spec.rb", "spec/monster/remote/filters/filter_spec.rb", "spec/monster/remote/filters/name_based_filter_spec.rb", "spec/monster/remote/sync_spec.rb", "spec/monster/remote/wrappers/net_ftp_spec.rb", "spec/spec_helper.rb"]
24
+ s.test_files = ["spec/monster/remote/cli_spec.rb", "spec/monster/remote/configuration_spec.rb", "spec/monster/remote/filters/filter_spec.rb", "spec/monster/remote/filters/name_based_filter_spec.rb", "spec/monster/remote/sync_spec.rb", "spec/monster/remote/wrappers/net_ftp_spec.rb", "spec/spec_helper.rb"]
25
+
26
+
27
+ s.executables = ["monster_remote"]
28
+
29
+ end
@@ -0,0 +1,237 @@
1
+ module Monster
2
+ module Remote
3
+
4
+ describe CLI do
5
+
6
+ module ::Kernel
7
+ def rescuing_exit
8
+ yield
9
+ rescue SystemExit
10
+ end
11
+ end
12
+
13
+ def executable
14
+ File.join(File.expand_path("../../../../", __FILE__), "bin/monster_remote")
15
+ end
16
+
17
+ before(:all) do
18
+ @current_dir = Dir.pwd
19
+ @net_ftp = Monster::Remote::Wrappers::NetFTP
20
+ end
21
+
22
+ before do
23
+ @syncer = double("sync contract").as_null_object
24
+ @syncer.stub(:new).and_return(@syncer)
25
+ @out = double("out contract").as_null_object
26
+ @in = double("in contract").as_null_object
27
+ @password = "123"
28
+ @in.stub(:gets).and_return(@password)
29
+ @wrapper = double("fake wrapper").as_null_object
30
+
31
+ @cli = CLI.new(@syncer, @out, @in)
32
+ end
33
+
34
+ it "-v returns the version" do
35
+ rescuing_exit { @cli.run(["-v"]) == Monster::Remote::VERSION }
36
+ end
37
+
38
+ context "-p (wait for passowrd)" do
39
+
40
+ it "calls #print with 'password:' on the output" do
41
+ rescuing_exit do
42
+ @out.should_receive(:print).with("password:")
43
+ @cli.run(["-p"])
44
+ end
45
+ end
46
+
47
+ it "calls #gets on the input" do
48
+ rescuing_exit do
49
+ @in.should_receive(:gets)
50
+ @cli.run(["-p"])
51
+ end
52
+ end
53
+
54
+ end# wait for password
55
+
56
+ context "using the 'sync' interface" do
57
+
58
+ before do
59
+ @dirname = File.basename(@current_dir)
60
+ Monster::Remote::Wrappers::NetFTP = @wrapper
61
+ end
62
+
63
+ it "--ftp -p awaits for password even if another flags/options" do
64
+ rescuing_exit do
65
+ @in.should_receive(:gets)
66
+ @cli.run(["--ftp", "-p"])
67
+ end
68
+ end
69
+
70
+ it "--ftp and ::new parameters" do
71
+ @syncer.should_receive(:new).with(@wrapper, @current_dir, @dirname, nil)
72
+ rescuing_exit do
73
+ @cli.run(["--ftp"])
74
+ end
75
+ end
76
+
77
+ it "uses NetFTP as default wrapper" do
78
+ @syncer.should_receive(:new).with(@wrapper, @current_dir, @dirname, nil)
79
+ rescuing_exit do
80
+ @cli.run(["-p"])
81
+ end
82
+ end
83
+
84
+ it "--verbose turn on the syncer verbosity" do
85
+ out = STDOUT.clone
86
+ STDOUT = double("omg my own stdout")
87
+ @syncer.should_receive(:new).with(@wrapper, @current_dir, @dirname, STDOUT)
88
+ rescuing_exit do
89
+ @cli.run(["--verbose"])
90
+ end
91
+ STDOUT = out
92
+ end
93
+
94
+ it "-l allow configure the local dir" do
95
+ local_dir = "opa/lele"
96
+ @syncer.should_receive(:new).with(@wrapper, local_dir, File.basename(local_dir), nil)
97
+ rescuing_exit do
98
+ @cli.run(["-l", local_dir])
99
+ end
100
+ end
101
+
102
+ it "-r allow specify the remote dir" do
103
+ remote = "test/omg/gogo"
104
+ @syncer.should_receive(:new).with(@wrapper, @current_dir, remote, nil)
105
+ rescuing_exit do
106
+ @cli.run(["-r", remote])
107
+ end
108
+ end
109
+
110
+ it "start sync with default configurations" do
111
+ @syncer.should_receive(:start).with(nil, nil, "localhost", 21)
112
+ rescuing_exit do
113
+ @cli.run
114
+ end
115
+ end
116
+
117
+ it "-H allow specify the server host" do
118
+ host = "borba"
119
+ @syncer.should_receive(:start).with(nil, nil, host, 21)
120
+ rescuing_exit do
121
+ @cli.run(["-H", host])
122
+ end
123
+ end
124
+
125
+ it "-P allow specify the host port" do
126
+ host = "borba"
127
+ port = "portalhes"
128
+ @syncer.should_receive(:start).with(nil, nil, host, port)
129
+ rescuing_exit do
130
+ @cli.run(["-H", host, "-P", port])
131
+ end
132
+ end
133
+
134
+ it "-u specify the user" do
135
+ user = "omg-my-user"
136
+ @syncer.should_receive(:start).with(user, nil, "localhost", 21)
137
+ rescuing_exit do
138
+ @cli.run(["-u", user])
139
+ end
140
+ end
141
+
142
+ it "-p specify the password" do
143
+ user = "omg-my-user"
144
+ @syncer.should_receive(:start).with(user, @password, "localhost", 21)
145
+ rescuing_exit do
146
+ @cli.run(["-u", user, "-p"])
147
+ end
148
+ end
149
+
150
+ end# sync
151
+
152
+ context "using configs from config file" do
153
+ before(:all) do
154
+ FileUtils.mkdir_p(spec_tmp) unless File.directory?(spec_tmp)
155
+ @file = File.join(spec_tmp, "_config.yml")
156
+ @pwd = Dir.pwd
157
+
158
+ Dir.chdir(spec_tmp)
159
+ file_content = "
160
+ monster:
161
+ remote:
162
+ host: host
163
+ port: 333
164
+ user: user
165
+ pass: false
166
+ local_dir: local
167
+ remote_dir: remote
168
+ "
169
+ File.open(@file, "w") do |f|
170
+ f.write(file_content)
171
+ end
172
+ end
173
+
174
+ it "calls #start with configs from file" do
175
+ @syncer.should_receive(:start).with("user", nil, "host", 333)
176
+ rescuing_exit do
177
+ @cli.run
178
+ end
179
+ end
180
+
181
+ it "calls ::new with configs from file" do
182
+ Monster::Remote::Wrappers::NetFTP = @wrapper
183
+ @syncer.should_receive(:new).with(@wrapper, "local", "remote", nil)
184
+ rescuing_exit do
185
+ @cli.run
186
+ end
187
+ end
188
+
189
+ it "command line options should override configs" do
190
+ @syncer.should_receive(:start).with("omg_my_user", nil, "monster", 333)
191
+ rescuing_exit do
192
+ @cli.run(["-u", "omg_my_user", "-H", "monster"])
193
+ end
194
+ end
195
+
196
+ it "should wait for password if configs says so" do
197
+ file_content = "
198
+ monster:
199
+ remote:
200
+ host: host
201
+ port: 333
202
+ user: user
203
+ pass: true
204
+ local_dir: local
205
+ remote_dir:
206
+ "
207
+ File.open(@file, "w") do |f|
208
+ f.write(file_content)
209
+ end
210
+ @syncer.should_receive(:start).with("user", @password, "host", 333)
211
+ rescuing_exit do
212
+ @cli.run
213
+ end
214
+ end
215
+
216
+ after(:all) do
217
+ FileUtils.rm_rf(spec_tmp) if File.directory?(spec_tmp)
218
+ Dir.chdir(@pwd)
219
+ end
220
+
221
+ end# _config.yml
222
+
223
+ context "executable" do
224
+
225
+ it "-v returns the version" do
226
+ `#{executable} -v`.strip.should == Monster::Remote::VERSION
227
+ end# -v
228
+
229
+ end# executable
230
+
231
+ after(:all) do
232
+ Monster::Remote::Wrappers::NetFTP = @net_ftp
233
+ end
234
+
235
+ end# CLI
236
+ end
237
+ end
@@ -0,0 +1,124 @@
1
+ module Monster
2
+ module Remote
3
+
4
+ describe Configuration do
5
+ def minimal_config_file(host="host")
6
+ "monster:
7
+ remote:
8
+ host: #{host}
9
+ "
10
+ end
11
+
12
+ def path_to(file="o_m_g.yml")
13
+ File.join(spec_tmp, file)
14
+ end
15
+
16
+ before(:all) do
17
+ FileUtils.mkdir_p(spec_tmp)
18
+ @file_content = minimal_config_file << "
19
+ port: 333
20
+ user: user
21
+ pass: true
22
+ local_dir: local
23
+ remote_dir:
24
+ "
25
+ end
26
+
27
+ it "ignores the abscence of config file" do
28
+ lambda { Configuration.new }.should_not raise_error
29
+ end
30
+
31
+ context "specific file on the constructor, properties" do
32
+
33
+ before(:all) do
34
+ @file = "o_m_g.yml"
35
+ File.open(path_to(@file), "w") do |f|
36
+ f.write(@file_content)
37
+ end
38
+ @conf = Configuration.new(path_to(@file))
39
+ end
40
+
41
+ it "#host" do
42
+ @conf.host.should == "host"
43
+ end
44
+
45
+ it "#port" do
46
+ @conf.port.should == 333
47
+ end
48
+
49
+ it "#user" do
50
+ @conf.user.should == "user"
51
+ end
52
+
53
+ it "#password_required?" do
54
+ @conf.password_required?.should be_true
55
+ end
56
+
57
+ it "#local_dir" do
58
+ @conf.local_dir.should == "local"
59
+ end
60
+
61
+ it "#remote_dir" do
62
+ @conf.remote_dir.should be_nil
63
+ end
64
+
65
+ after(:all) do
66
+ FileUtils.rm(path_to(@file)) if File.exists?(path_to(@file))
67
+ end
68
+
69
+ end# specific file
70
+
71
+ context "uses _config.yml as default configuration" do
72
+
73
+ before(:all) do
74
+ @pwd = Dir.pwd
75
+ Dir.chdir(spec_tmp)
76
+ @file = "_config.yml"
77
+ File.open(path_to(@file), "w") do |f|
78
+ f.write(minimal_config_file("borba"))
79
+ end
80
+ @conf = Configuration.new
81
+ end
82
+
83
+ it "gets configs from _config.yml by default" do
84
+ @conf.host.should == "borba"
85
+ end
86
+
87
+ after(:all) do
88
+ FileUtils.rm(@file)
89
+ Dir.chdir(@pwd)
90
+ end
91
+
92
+ end# defaults to _config.yml
93
+
94
+ context "uses .monster.yml as fallback for _config.yml" do
95
+
96
+ before(:all) do
97
+ @pwd = Dir.pwd
98
+ Dir.chdir(spec_tmp)
99
+ @file = ".monster.yml"
100
+ File.open(path_to(@file), "w") do |f|
101
+ f.write(minimal_config_file("lacatumba"))
102
+ end
103
+ @conf = Configuration.new
104
+ end
105
+
106
+ it "gets configs from .monster.yml" do
107
+ @conf.host.should == "lacatumba"
108
+ end
109
+
110
+ after(:all) do
111
+ FileUtils.rm(@file)
112
+ Dir.chdir(@pwd)
113
+ end
114
+
115
+ end
116
+
117
+ after(:all) do
118
+ FileUtils.rm_rf(spec_tmp) if File.exists?(spec_tmp)
119
+ end
120
+
121
+ end# Configuration
122
+
123
+ end# Remote
124
+ end# Monpatster
@@ -0,0 +1,16 @@
1
+ module Monster
2
+ module Remote
3
+ module Filters
4
+
5
+ describe Filter do
6
+
7
+ it "accept a block with rejection logic, the list is passed as argument" do
8
+ filter = Filter.new
9
+ filter.reject lambda{ |entries| entries.reject{|entry| entry != "borba"} }
10
+ filter.filter(["a", "b", "borba"]).should == ["borba"]
11
+ end
12
+ end # describe Filter
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,57 @@
1
+ module Monster
2
+ module Remote
3
+ module Filters
4
+
5
+ describe NameBasedFilter do
6
+
7
+ def create_fake_dirs
8
+ @all_entries.each do |dir|
9
+ FileUtils.mkdir_p(File.join(@root_dir, dir))
10
+ end
11
+ end
12
+
13
+ before(:all) do
14
+ FakeFS.activate!
15
+ @root_dir = "/jajaja"
16
+ @allowed = ["I_CAN_BE_FILE", "ME_CAN"]
17
+ @forbidden = [".", "..", ".mafagafanho", "borba"]
18
+ @all_entries = @allowed + @forbidden
19
+ create_fake_dirs
20
+ end
21
+
22
+ before do
23
+ @filter = subject
24
+ end
25
+
26
+ it "accepts a dir name and return only allowed entries" do
27
+ @filter.reject @forbidden
28
+ @filter.filter(@root_dir).should == @allowed
29
+ end
30
+
31
+ context "#reject, configuring forbbiden names" do
32
+
33
+ it "accept string as parameter" do
34
+ @filter.reject "."
35
+ @filter.reject ".."
36
+ @filter.filter(["a", ".", ".."]).should == ["a"]
37
+ end
38
+
39
+ it "or any Enumerable" do
40
+ rejecting = ["a", "bb", "ccc"]
41
+ @filter.reject rejecting
42
+ @filter.filter(rejecting + ["opalhes"]).should == ["opalhes"]
43
+ end
44
+
45
+ it "accept a block with rejection logic, the list is passed as argument" do
46
+ @filter.reject lambda{ |entries| entries.reject{|entry| entry != "borba"} }
47
+ @filter.filter(["a", "b", "borba"]).should == ["borba"]
48
+ end
49
+ end # #reject
50
+
51
+ after(:all) do
52
+ FakeFS.deactivate!
53
+ end
54
+ end # describe ContentNameBasedFilter
55
+ end
56
+ end
57
+ end