em-ftp-client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README +38 -0
- data/Rakefile +10 -0
- data/em-ftp-client.gemspec +30 -0
- data/lib/em-ftp-client.rb +7 -0
- data/lib/em-ftp-client/control_connection.rb +274 -0
- data/lib/em-ftp-client/data_connection.rb +31 -0
- data/lib/em-ftp-client/session.rb +53 -0
- data/lib/em-ftp-client/sync_session.rb +57 -0
- data/lib/em-ftp-client/version.rb +7 -0
- data/test/control_connection_test.rb +196 -0
- data/test/helper.rb +9 -0
- metadata +165 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2011 Ben Hughes
|
2
|
+
Copyright (c) 2011 NabeWise Media, Inc.
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
21
|
+
|
data/README
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
em-ftp-client is a simple EventMachine based FTP Client supporting operations
|
2
|
+
on stream data. The primary interface is EventMachine::FtpClient::Session. A
|
3
|
+
standard usage would look like:
|
4
|
+
|
5
|
+
require 'eventmachine'
|
6
|
+
require 'em-ftp-client'
|
7
|
+
|
8
|
+
EM.run do
|
9
|
+
EM::FtpClient::Session.new("0.0.0.0",
|
10
|
+
:username => "test",
|
11
|
+
:password => "1234") do |ftp|
|
12
|
+
|
13
|
+
ftp.list do |l1|
|
14
|
+
puts l1
|
15
|
+
puts
|
16
|
+
ftp.cwd("files") do
|
17
|
+
ftp.list do |l2|
|
18
|
+
puts l2
|
19
|
+
puts
|
20
|
+
ftp.stream {|d| puts "STREAMING: #{d.inspect}" }
|
21
|
+
ftp.get("two.txt") do |t1|
|
22
|
+
puts "COMPLETED"
|
23
|
+
puts t1
|
24
|
+
puts
|
25
|
+
EM.stop
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
This library also includes the class SyncSession, which uses fibers and works
|
34
|
+
with em-synchrony.
|
35
|
+
|
36
|
+
TODO:
|
37
|
+
|
38
|
+
Add support for put/STOR operations
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "em-ftp-client/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "em-ftp-client"
|
7
|
+
s.version = Em::Ftp::Client::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Ben Hughes"]
|
10
|
+
s.email = ["ben@pixelmachine.org"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{EventMachine FTP client}
|
13
|
+
s.description = %q{An FTP client designed to work well with EventMachine}
|
14
|
+
|
15
|
+
s.rubyforge_project = "em-ftp-client"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
|
23
|
+
s.add_dependency('eventmachine')
|
24
|
+
s.add_dependency('net-ftp-list')
|
25
|
+
|
26
|
+
s.add_development_dependency('shoulda')
|
27
|
+
s.add_development_dependency('mocha')
|
28
|
+
s.add_development_dependency('redgreen')
|
29
|
+
s.add_development_dependency('rake')
|
30
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module FtpClient
|
3
|
+
class ControlConnection < Connection
|
4
|
+
include Protocols::LineText2
|
5
|
+
|
6
|
+
attr_accessor :username, :password
|
7
|
+
|
8
|
+
attr_reader :responder
|
9
|
+
|
10
|
+
class InvalidResponseFormat < RuntimeError; end
|
11
|
+
|
12
|
+
class Response
|
13
|
+
attr_reader :code, :body, :parent
|
14
|
+
def initialize(code=nil, body=nil)
|
15
|
+
@code = code
|
16
|
+
@body = body
|
17
|
+
@empty = true
|
18
|
+
@complete = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def complete?
|
22
|
+
@complete
|
23
|
+
end
|
24
|
+
|
25
|
+
def mark?
|
26
|
+
code && code[0,1] == "1"
|
27
|
+
end
|
28
|
+
|
29
|
+
def success?
|
30
|
+
complete? && code && (code[0,1] == "2" || code[0,1] == "3")
|
31
|
+
end
|
32
|
+
|
33
|
+
def failure?
|
34
|
+
complete? && code && (code[0,1] == "4" || code[0,1] == "5")
|
35
|
+
end
|
36
|
+
|
37
|
+
def <<(line)
|
38
|
+
# For an empty response
|
39
|
+
if @empty
|
40
|
+
# If the response is a valid format
|
41
|
+
if line =~ /^[1-5]\d{2}( |-)/
|
42
|
+
# If the response is multiline
|
43
|
+
if line[3,1] == '-'
|
44
|
+
@code, @body = line.chomp.split('-', 2)
|
45
|
+
# If the response is single line
|
46
|
+
elsif line[3,1] == ' '
|
47
|
+
@code, @body = line.chomp.split(' ', 2)
|
48
|
+
@complete = true
|
49
|
+
end
|
50
|
+
@empty = false
|
51
|
+
else
|
52
|
+
raise InvalidResponseFormat.new(line.chomp)
|
53
|
+
end
|
54
|
+
# If the response is a continuation of a multiline response
|
55
|
+
else
|
56
|
+
# If this response is terminal
|
57
|
+
if line[0..3] == "#{code} "
|
58
|
+
@body += "\n#{line.chomp.split(' ', 2)[1]}"
|
59
|
+
@complete = true
|
60
|
+
# Otherwise continue hoping and wishing for it to be terminated
|
61
|
+
else
|
62
|
+
@body += "\n#{line.chomp}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
line
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize
|
71
|
+
end
|
72
|
+
|
73
|
+
def post_init
|
74
|
+
@data_connection = nil
|
75
|
+
@response = Response.new
|
76
|
+
@responder = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def connection_completed
|
80
|
+
@responder = :receive_greetings
|
81
|
+
end
|
82
|
+
|
83
|
+
def error(e)
|
84
|
+
@errback.call(e) if @errback
|
85
|
+
end
|
86
|
+
|
87
|
+
def receive_line(line)
|
88
|
+
@response << line
|
89
|
+
if @response.complete?
|
90
|
+
# get a new fresh response ready
|
91
|
+
old_response = @response
|
92
|
+
@response = Response.new
|
93
|
+
|
94
|
+
# dispatch appropriately
|
95
|
+
if old_response.success?
|
96
|
+
send(@responder, old_response) if @responder
|
97
|
+
elsif old_response.mark?
|
98
|
+
#maybe notice the mark or something
|
99
|
+
elsif old_response.failure?
|
100
|
+
error(old_response)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
rescue InvalidResponseFormat => e
|
104
|
+
@response = Response.new
|
105
|
+
error(e)
|
106
|
+
end
|
107
|
+
|
108
|
+
def data_connection_closed(data)
|
109
|
+
@data_buffer = data
|
110
|
+
@data_connection = nil
|
111
|
+
send(@responder) if @responder
|
112
|
+
end
|
113
|
+
|
114
|
+
def callback(&blk)
|
115
|
+
@callback = blk
|
116
|
+
end
|
117
|
+
|
118
|
+
def errback(&blk)
|
119
|
+
@errback = blk
|
120
|
+
end
|
121
|
+
|
122
|
+
def call_callback(*args)
|
123
|
+
old_callback = @callback
|
124
|
+
@callback = nil
|
125
|
+
old_callback.call(*args) if old_callback
|
126
|
+
end
|
127
|
+
|
128
|
+
def call_errback(*args)
|
129
|
+
@errback.call(*args) if @errback
|
130
|
+
@errback = nil
|
131
|
+
end
|
132
|
+
|
133
|
+
# commands
|
134
|
+
def user(name)
|
135
|
+
send_data("USER #{name}\r\n")
|
136
|
+
@responder = :user_response
|
137
|
+
end
|
138
|
+
|
139
|
+
def pass(word)
|
140
|
+
send_data("PASS #{word}\r\n")
|
141
|
+
@responder = :password_response
|
142
|
+
end
|
143
|
+
|
144
|
+
def type(t)
|
145
|
+
send_data("TYPE #{t}\r\n")
|
146
|
+
@responder = :type_response
|
147
|
+
end
|
148
|
+
|
149
|
+
def cwd(dir)
|
150
|
+
send_data("CWD #{dir}\r\n")
|
151
|
+
@responder = :cwd_response
|
152
|
+
end
|
153
|
+
|
154
|
+
def pwd
|
155
|
+
send_data("PWD\r\n")
|
156
|
+
@responder = :pwd_response
|
157
|
+
end
|
158
|
+
|
159
|
+
def pasv
|
160
|
+
send_data("PASV\r\n")
|
161
|
+
@responder = :pasv_response
|
162
|
+
end
|
163
|
+
|
164
|
+
def retr(file)
|
165
|
+
send_data("RETR #{file}\r\n")
|
166
|
+
@responder = :retr_response
|
167
|
+
end
|
168
|
+
|
169
|
+
def list
|
170
|
+
send_data("LIST\r\n")
|
171
|
+
@responder = :list_response
|
172
|
+
end
|
173
|
+
|
174
|
+
# handlers
|
175
|
+
|
176
|
+
# Called after initial connection
|
177
|
+
def receive_greetings(banner)
|
178
|
+
if banner.code == "220"
|
179
|
+
user username
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Called when a response for the USER verb is received
|
184
|
+
def user_response(response)
|
185
|
+
pass password
|
186
|
+
end
|
187
|
+
|
188
|
+
# Called when a response for the PASS verb is received
|
189
|
+
def password_response(response)
|
190
|
+
type "I"
|
191
|
+
end
|
192
|
+
|
193
|
+
# Called when a response for the TYPE verb is received
|
194
|
+
def type_response(response)
|
195
|
+
@responder = nil
|
196
|
+
call_callback
|
197
|
+
end
|
198
|
+
|
199
|
+
# Called when a response for the CWD or CDUP is received
|
200
|
+
def cwd_response(response)
|
201
|
+
@responder = nil
|
202
|
+
call_callback
|
203
|
+
end
|
204
|
+
|
205
|
+
# Called when a response for the PWD verb is received
|
206
|
+
#
|
207
|
+
# Calls out with the result to the callback given to pwd
|
208
|
+
def pwd_response(response)
|
209
|
+
@responder = nil
|
210
|
+
call_callback(response.body)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Called when a response for the PASV verb is received
|
214
|
+
#
|
215
|
+
# Opens a new data connection and executes the callback
|
216
|
+
def pasv_response(response)
|
217
|
+
@responder = nil
|
218
|
+
if response.code == "227"
|
219
|
+
if m = /(\d{1,3},\d{1,3},\d{1,3},\d{1,3}),(\d+),(\d+)/.match(response.body)
|
220
|
+
host_ip = m[1].gsub(",", ".")
|
221
|
+
host_port = m[2].to_i*256 + m[3].to_i
|
222
|
+
pasv_callback = @callback
|
223
|
+
@data_connection = EM.connect(host_ip, host_port, DataConnection)
|
224
|
+
@data_connection.on_connect &pasv_callback
|
225
|
+
@data_connection.callback {|data| data_connection_closed(data) }
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def retr_response(response=nil)
|
231
|
+
if response && response.code != "226"
|
232
|
+
@data_connection.close_connection
|
233
|
+
@responder = nil
|
234
|
+
error(response)
|
235
|
+
end
|
236
|
+
|
237
|
+
if response && @data_connection
|
238
|
+
#well we still gots to wait for the file
|
239
|
+
elsif @data_connection
|
240
|
+
#well we need to wait for a response
|
241
|
+
else
|
242
|
+
@responder = nil
|
243
|
+
old_data_buffer = @data_buffer
|
244
|
+
@data_buffer = nil
|
245
|
+
call_callback(old_data_buffer)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def list_response(response=nil)
|
250
|
+
if response && response.code != "226"
|
251
|
+
@data_connection.close_connection
|
252
|
+
@responder = nil
|
253
|
+
error(response)
|
254
|
+
end
|
255
|
+
|
256
|
+
if response && @data_connection
|
257
|
+
#well we still gots to wait for the file
|
258
|
+
elsif @data_connection
|
259
|
+
#well we need to wait for a response
|
260
|
+
else
|
261
|
+
@responder = nil
|
262
|
+
old_data_buffer = @data_buffer
|
263
|
+
@data_buffer = nil
|
264
|
+
# parse it into a real form
|
265
|
+
file_list = old_data_buffer.split("\r\n").map do |line|
|
266
|
+
::Net::FTP::List.parse(line)
|
267
|
+
end
|
268
|
+
call_callback(file_list)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module FtpClient
|
3
|
+
class DataConnection < Connection
|
4
|
+
include Deferrable
|
5
|
+
|
6
|
+
def on_connect(&blk); @on_connect = blk; end
|
7
|
+
|
8
|
+
def stream(&blk); @stream = blk; end
|
9
|
+
|
10
|
+
def post_init
|
11
|
+
@buf = ''
|
12
|
+
end
|
13
|
+
|
14
|
+
def connection_completed
|
15
|
+
@on_connect.call(self) if @on_connect
|
16
|
+
end
|
17
|
+
|
18
|
+
def receive_data(data)
|
19
|
+
@buf += data
|
20
|
+
if @stream
|
21
|
+
@stream.call(@buf)
|
22
|
+
@buf = ''
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def unbind
|
27
|
+
succeed(@buf)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module FtpClient
|
3
|
+
# Main class for interacting with a server
|
4
|
+
class Session
|
5
|
+
attr_accessor :username, :password, :port
|
6
|
+
|
7
|
+
attr_reader :control_connection
|
8
|
+
|
9
|
+
def initialize(url, options={}, &cb)
|
10
|
+
self.username = options[:username] || "anonymous"
|
11
|
+
self.password = options[:password] || "anonymous"
|
12
|
+
self.port = options[:port] || 21
|
13
|
+
|
14
|
+
@control_connection = EM.connect(url, port, ControlConnection)
|
15
|
+
@control_connection.username = username
|
16
|
+
@control_connection.password = password
|
17
|
+
@control_connection.callback do
|
18
|
+
cb.call(self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def pwd(&cb)
|
23
|
+
control_connection.callback(&cb)
|
24
|
+
control_connection.pwd
|
25
|
+
end
|
26
|
+
|
27
|
+
def cwd(dir, &cb)
|
28
|
+
control_connection.callback(&cb)
|
29
|
+
control_connection.cwd(dir)
|
30
|
+
end
|
31
|
+
|
32
|
+
def list(&cb)
|
33
|
+
control_connection.callback do
|
34
|
+
control_connection.callback(&cb)
|
35
|
+
control_connection.list
|
36
|
+
end
|
37
|
+
control_connection.pasv
|
38
|
+
end
|
39
|
+
|
40
|
+
def stream(&cb); @stream = cb; end
|
41
|
+
|
42
|
+
def get(file, &cb)
|
43
|
+
control_connection.callback do |data_connection|
|
44
|
+
data_connection.stream(&@stream) if @stream
|
45
|
+
control_connection.callback(&cb)
|
46
|
+
control_connection.retr file
|
47
|
+
end
|
48
|
+
control_connection.pasv
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
begin
|
2
|
+
require 'fiber'
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
module FtpClient
|
6
|
+
class SyncSession < Session
|
7
|
+
def initialize(url, options={})
|
8
|
+
f = Fiber.current
|
9
|
+
super url, options do |conn|
|
10
|
+
f.resume(conn)
|
11
|
+
end
|
12
|
+
|
13
|
+
Fiber.yield
|
14
|
+
end
|
15
|
+
|
16
|
+
def pwd
|
17
|
+
f = Fiber.current
|
18
|
+
super do |arg|
|
19
|
+
f.resume(arg)
|
20
|
+
end
|
21
|
+
|
22
|
+
Fiber.yield
|
23
|
+
end
|
24
|
+
|
25
|
+
def cwd(dir)
|
26
|
+
f = Fiber.current
|
27
|
+
super dir do
|
28
|
+
f.resume
|
29
|
+
end
|
30
|
+
|
31
|
+
Fiber.yield
|
32
|
+
end
|
33
|
+
|
34
|
+
def list
|
35
|
+
f = Fiber.current
|
36
|
+
super do |data|
|
37
|
+
f.resume(data)
|
38
|
+
end
|
39
|
+
|
40
|
+
Fiber.yield
|
41
|
+
end
|
42
|
+
|
43
|
+
def get(file)
|
44
|
+
f = Fiber.current
|
45
|
+
|
46
|
+
super file do |data|
|
47
|
+
f.resume(data)
|
48
|
+
end
|
49
|
+
|
50
|
+
Fiber.yield
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
rescue LoadError
|
56
|
+
# Could not load fiber support
|
57
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "helper")
|
2
|
+
|
3
|
+
class ControlConnectionTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@control_connection = EventMachine::FtpClient::ControlConnection.new(:foo)
|
6
|
+
end
|
7
|
+
|
8
|
+
context "Response" do
|
9
|
+
setup do
|
10
|
+
@response = EM::FtpClient::ControlConnection::Response.new
|
11
|
+
end
|
12
|
+
|
13
|
+
context "single line" do
|
14
|
+
context "success response" do
|
15
|
+
setup do
|
16
|
+
@code = "200"
|
17
|
+
@body = "OK Yeah"
|
18
|
+
@response << "#{@code} #{@body}\r\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
should "be complete" do
|
22
|
+
assert @response.complete?
|
23
|
+
end
|
24
|
+
|
25
|
+
should "be successful" do
|
26
|
+
assert @response.success?
|
27
|
+
assert !@response.failure?
|
28
|
+
end
|
29
|
+
|
30
|
+
should "have code and body" do
|
31
|
+
assert_equal @code, @response.code
|
32
|
+
assert_equal @body, @response.body
|
33
|
+
end
|
34
|
+
|
35
|
+
should "not be a mark" do
|
36
|
+
assert !@response.mark?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "failure response" do
|
41
|
+
setup do
|
42
|
+
@code = "500"
|
43
|
+
@body = "No Soup For You"
|
44
|
+
@response << "#{@code} #{@body}\r\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
should "be complete" do
|
48
|
+
assert @response.complete?
|
49
|
+
end
|
50
|
+
|
51
|
+
should "be a failure" do
|
52
|
+
assert !@response.success?
|
53
|
+
assert @response.failure?
|
54
|
+
end
|
55
|
+
|
56
|
+
should "have code and body" do
|
57
|
+
assert_equal @code, @response.code
|
58
|
+
assert_equal @body, @response.body
|
59
|
+
end
|
60
|
+
|
61
|
+
should "not be a mark" do
|
62
|
+
assert !@response.mark?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context "mark response" do
|
67
|
+
setup do
|
68
|
+
@code = "150"
|
69
|
+
@body = "Roger that"
|
70
|
+
@response << "#{@code} #{@body}\r\n"
|
71
|
+
end
|
72
|
+
|
73
|
+
should "be complete" do
|
74
|
+
assert @response.complete?
|
75
|
+
end
|
76
|
+
|
77
|
+
should "not be a failure or a success" do
|
78
|
+
assert !@response.success?
|
79
|
+
assert !@response.failure?
|
80
|
+
end
|
81
|
+
|
82
|
+
should "have code and body" do
|
83
|
+
assert_equal @code, @response.code
|
84
|
+
assert_equal @body, @response.body
|
85
|
+
end
|
86
|
+
|
87
|
+
should "be a mark" do
|
88
|
+
assert @response.mark?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "multiline" do
|
94
|
+
context "valid" do
|
95
|
+
should "work" do
|
96
|
+
@code = "226"
|
97
|
+
@body = ["Oh yeah", "Its on", "And done"]
|
98
|
+
@response << "#{@code}-#{@body[0]}\r\n"
|
99
|
+
assert !@response.complete?
|
100
|
+
@response << "#{@body[1]}\r\n"
|
101
|
+
assert !@response.complete?
|
102
|
+
@response << "#{@code} #{@body[2]}\r\n"
|
103
|
+
assert @response.complete?
|
104
|
+
assert_equal @code, @response.code
|
105
|
+
assert_equal @body.join("\n"), @response.body
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "attempted to be closed with a different code" do
|
110
|
+
should "not complete" do
|
111
|
+
@code = "226"
|
112
|
+
@response << "#{@code}-Foo\r\n"
|
113
|
+
assert !@response.complete?
|
114
|
+
@response << "227 Done\r\n"
|
115
|
+
assert !@response.complete?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "invalid response" do
|
121
|
+
should "raise an invalid response format error" do
|
122
|
+
assert_raise EM::FtpClient::ControlConnection::InvalidResponseFormat do
|
123
|
+
@response << "THIS IS SPARTA\r\n"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_basic_login
|
130
|
+
r = EM::FtpClient::ControlConnection::Response
|
131
|
+
@control_connection.username = "kingsly"
|
132
|
+
@control_connection.password = "password"
|
133
|
+
|
134
|
+
@control_connection.stubs(:send_data => true)
|
135
|
+
|
136
|
+
@control_connection.connection_completed
|
137
|
+
assert_equal :receive_greetings, @control_connection.responder
|
138
|
+
@control_connection.receive_greetings(r.new("220", "Come on in"))
|
139
|
+
assert_equal :user_response, @control_connection.responder
|
140
|
+
@control_connection.user_response(r.new("331", "Cool"))
|
141
|
+
assert_equal :password_response, @control_connection.responder
|
142
|
+
@control_connection.password_response(r.new("230", "Awesome"))
|
143
|
+
assert_equal :type_response, @control_connection.responder
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_protocol_interaction
|
147
|
+
@control_connection.username = "kingsly"
|
148
|
+
@control_connection.password = "password"
|
149
|
+
|
150
|
+
@control_connection.connection_completed
|
151
|
+
|
152
|
+
started = false
|
153
|
+
working_dir = nil
|
154
|
+
|
155
|
+
@data_host = "127.0.0.1"
|
156
|
+
@data_port = 56789
|
157
|
+
|
158
|
+
@data_host_string = "127,0,0,1,221,213"
|
159
|
+
|
160
|
+
@data_connection = EM::FtpClient::DataConnection.new(:foo)
|
161
|
+
EventMachine.expects(:connect).with(@data_host, @data_port,
|
162
|
+
EM::FtpClient::DataConnection).
|
163
|
+
returns(@data_connection)
|
164
|
+
|
165
|
+
pasv_callback_called = false
|
166
|
+
|
167
|
+
[[nil, nil, "220 Come on in"],
|
168
|
+
[nil, "USER kingsly", "331 Cool"],
|
169
|
+
[nil, "PASS password", "230 Awesome"],
|
170
|
+
[nil, "TYPE I", "200 So Say We All", lambda { started = true }],
|
171
|
+
[[:pwd], "PWD", "257 \"/foo\"", lambda{|d| working_dir = d }],
|
172
|
+
[[:pasv], "PASV", "227 =#{@data_host_string}", lambda{ pasv_callback_called }]].each do |set|
|
173
|
+
@control_connection.expects(:send_data).with(set[1]+"\r\n") if set[1]
|
174
|
+
end.each do |set|
|
175
|
+
@control_connection.callback(&set[3]) if set[3]
|
176
|
+
@control_connection.send(*set[0]) if set[0]
|
177
|
+
@control_connection.receive_line(set[2]+"\r\n") if set[2]
|
178
|
+
end
|
179
|
+
|
180
|
+
@control_connection.expects(:send_data).with("RETR foo.txt\r\n")
|
181
|
+
@control_connection.retr("foo.txt")
|
182
|
+
|
183
|
+
retr_completed = false
|
184
|
+
@control_connection.callback {|data| assert_equal "Bar", data; retr_completed = true }
|
185
|
+
assert !retr_completed
|
186
|
+
@control_connection.receive_line("226 Hooray\r\n")
|
187
|
+
assert !retr_completed
|
188
|
+
@data_connection.receive_data("Bar")
|
189
|
+
@data_connection.unbind
|
190
|
+
assert retr_completed
|
191
|
+
|
192
|
+
assert started
|
193
|
+
assert_equal "\"/foo\"", working_dir
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
data/test/helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-ftp-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Ben Hughes
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-06-29 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: eventmachine
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: net-ftp-list
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: shoulda
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :development
|
62
|
+
version_requirements: *id003
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: mocha
|
65
|
+
prerelease: false
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 3
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
type: :development
|
76
|
+
version_requirements: *id004
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
name: redgreen
|
79
|
+
prerelease: false
|
80
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
type: :development
|
90
|
+
version_requirements: *id005
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: rake
|
93
|
+
prerelease: false
|
94
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
hash: 3
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
type: :development
|
104
|
+
version_requirements: *id006
|
105
|
+
description: An FTP client designed to work well with EventMachine
|
106
|
+
email:
|
107
|
+
- ben@pixelmachine.org
|
108
|
+
executables: []
|
109
|
+
|
110
|
+
extensions: []
|
111
|
+
|
112
|
+
extra_rdoc_files: []
|
113
|
+
|
114
|
+
files:
|
115
|
+
- .gitignore
|
116
|
+
- Gemfile
|
117
|
+
- LICENSE
|
118
|
+
- README
|
119
|
+
- Rakefile
|
120
|
+
- em-ftp-client.gemspec
|
121
|
+
- lib/em-ftp-client.rb
|
122
|
+
- lib/em-ftp-client/control_connection.rb
|
123
|
+
- lib/em-ftp-client/data_connection.rb
|
124
|
+
- lib/em-ftp-client/session.rb
|
125
|
+
- lib/em-ftp-client/sync_session.rb
|
126
|
+
- lib/em-ftp-client/version.rb
|
127
|
+
- test/control_connection_test.rb
|
128
|
+
- test/helper.rb
|
129
|
+
has_rdoc: true
|
130
|
+
homepage: ""
|
131
|
+
licenses: []
|
132
|
+
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options: []
|
135
|
+
|
136
|
+
require_paths:
|
137
|
+
- lib
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
139
|
+
none: false
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
hash: 3
|
144
|
+
segments:
|
145
|
+
- 0
|
146
|
+
version: "0"
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
none: false
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
hash: 3
|
153
|
+
segments:
|
154
|
+
- 0
|
155
|
+
version: "0"
|
156
|
+
requirements: []
|
157
|
+
|
158
|
+
rubyforge_project: em-ftp-client
|
159
|
+
rubygems_version: 1.4.1
|
160
|
+
signing_key:
|
161
|
+
specification_version: 3
|
162
|
+
summary: EventMachine FTP client
|
163
|
+
test_files:
|
164
|
+
- test/control_connection_test.rb
|
165
|
+
- test/helper.rb
|