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 ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+
6
+ test_client.rb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in em-ftp-client.gemspec
4
+ gemspec
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,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.pattern = 'test/**/*_test.rb'
8
+ test.verbose = true
9
+ end
10
+
@@ -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,7 @@
1
+ require 'eventmachine'
2
+ require 'net/ftp/list'
3
+
4
+ require 'em-ftp-client/control_connection'
5
+ require 'em-ftp-client/data_connection'
6
+ require 'em-ftp-client/session'
7
+ require 'em-ftp-client/sync_session'
@@ -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,7 @@
1
+ module Em
2
+ module Ftp
3
+ module Client
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ 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
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require File.join(File.dirname(__FILE__), "..", "lib", "em-ftp-client")
5
+
6
+ require 'test/unit'
7
+ require 'mocha'
8
+ require 'shoulda'
9
+ require 'redgreen' unless '1.9'.respond_to?(:force_encoding)
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