ftp_sync 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,24 @@
1
+ = FtpSync
2
+
3
+ A Ruby library for recursively downloading and uploading directories to/from ftp
4
+ servers. Also supports uploading and downloading a list of files relative to
5
+ the local/remote roots. You can specify a timestamp to only download files
6
+ newer than that timestamp, or only download files newer than their local copy.
7
+
8
+ This was originally written to provide the functionality I needed for Munkey,
9
+ a tool for tracking changes on FTP servers with git.
10
+
11
+ Allows you to supply a 'ignore' class that dictates whether a file is excluded
12
+ from upload / download.
13
+
14
+ == Quickstart
15
+
16
+ ftp = FtpSync.new 'my.ftp.server.com', 'username', 'password'
17
+ ftp.pull_dir '/tmp/syncdir', 'path/on/server'
18
+
19
+ make some changes on server
20
+
21
+ ftp.pull_dir '/tmp/syncdir', 'path/on/server', :since => true, :delete => true
22
+
23
+ :since => true - means only files which are newer than the local copy will be downloaded
24
+ :delete => true - means if the file is removed from the server, then the local copy is removed
data/lib/ftp_sync.rb ADDED
@@ -0,0 +1,189 @@
1
+ require 'net/ftp'
2
+ require 'rubygems'
3
+ require 'net/ftp/list'
4
+ require 'fileutils'
5
+
6
+ # A Ruby library for recursively downloading and uploading directories to/from ftp
7
+ # servers. Also supports uploading and downloading a list of files relative to
8
+ # the local/remote roots. You can specify a timestamp to only download files
9
+ # newer than that timestamp, or only download files newer than their local copy.
10
+ class FtpSync
11
+
12
+ attr_accessor :verbose, :server, :user, :password
13
+
14
+ # Creates a new instance for accessing a ftp server
15
+ # requires +server+, +user+, and +password+ options
16
+ # * :ignore - Accepts an instance of class which has an ignore? method, taking a path and returns true or false, for whether to ignore the file or not.
17
+ # * :verbose - Whether should be verbose
18
+ def initialize(server, user, password, options = {})
19
+ @server = server
20
+ @user = user
21
+ @password = password
22
+ @connection = nil
23
+ @ignore = options[:ignore]
24
+ @recursion_level = 0
25
+ @verbose = options[:verbose] || false
26
+ end
27
+
28
+ # Recursively pull down files
29
+ # :since => true - only pull down files newer than their local counterpart, or with a different filesize
30
+ # :since => Time.now - only pull down files newer than the supplied timestamp, or with a different filesize
31
+ # :delete => Remove local files which don't exist on the FTP server
32
+ # If a block is supplied then it will be called to remove a local file
33
+
34
+ def pull_dir(localpath, remotepath, options = {}, &block)
35
+ connect! unless @connection
36
+ @recursion_level += 1
37
+
38
+ todelete = Dir.glob(File.join(localpath, '*'))
39
+
40
+ tocopy = []
41
+ recurse = []
42
+
43
+ # To trigger error if path doesnt exist since list will
44
+ # just return and empty array
45
+ @connection.chdir(remotepath)
46
+
47
+ @connection.list(remotepath) do |e|
48
+ entry = Net::FTP::List.parse(e)
49
+
50
+ paths = [ File.join(localpath, entry.basename), "#{remotepath}/#{entry.basename}".gsub(/\/+/, '/') ]
51
+
52
+ if entry.dir?
53
+ recurse << paths
54
+ elsif entry.file?
55
+ if options[:since] == :src
56
+ tocopy << paths unless File.exist?(paths[0]) and entry.mtime < File.mtime(paths[0]) and entry.filesize == File.size(paths[0])
57
+ elsif options[:since].is_a?(Time)
58
+ tocopy << paths unless entry.mtime < options[:since] and File.exist?(paths[0]) and entry.filesize == File.size(paths[0])
59
+ else
60
+ tocopy << paths
61
+ end
62
+ end
63
+ todelete.delete paths[0]
64
+ end
65
+
66
+ tocopy.each do |paths|
67
+ localfile, remotefile = paths
68
+ unless should_ignore?(localfile)
69
+ @connection.get(remotefile, localfile)
70
+ log "Pulled file #{remotefile}"
71
+ end
72
+ end
73
+
74
+ recurse.each do |paths|
75
+ localdir, remotedir = paths
76
+ Dir.mkdir(localdir) unless File.exist?(localdir)
77
+ pull_dir(localdir, remotedir, options, &block)
78
+ end
79
+
80
+ if options[:delete]
81
+ todelete.each do |p|
82
+ block_given? ? yield(p) : FileUtils.rm_rf(p)
83
+ log "Removed path #{p}"
84
+ end
85
+ end
86
+
87
+ @recursion_level -= 1
88
+ close! if @recursion_level == 0
89
+ rescue Net::FTPPermError
90
+ close!
91
+ raise Net::FTPPermError
92
+ end
93
+
94
+ # Recursively push a local directory of files onto an FTP server
95
+ def push_dir(localpath, remotepath)
96
+ connect!
97
+
98
+ Dir.glob(File.join(localpath, '**', '*')) do |f|
99
+ f.gsub!("#{localpath}/", '')
100
+ local = File.join localpath, f
101
+ remote = "#{remotepath}/#{f}".gsub(/\/+/, '/')
102
+
103
+ if File.directory?(local)
104
+ @connection.mkdir remote rescue Net::FTPPermError
105
+ log "Created Remote Directory #{local}"
106
+ elsif File.file?(local)
107
+ @connection.put local, remote
108
+ log "Pushed file #{remote}"
109
+ end
110
+ end
111
+
112
+ close!
113
+ end
114
+
115
+ # Pull a supplied list of files from the remote ftp path into the local path
116
+ def pull_files(localpath, remotepath, filelist)
117
+ connect!
118
+ filelist.each do |f|
119
+ localdir = File.join(localpath, File.dirname(f))
120
+ FileUtils.mkdir_p localdir unless File.exist?(localdir)
121
+ @connection.get "#{remotepath}/#{f}", File.join(localpath, f)
122
+ log "Pulled file #{remotepath}/#{f}"
123
+ end
124
+ close!
125
+ end
126
+
127
+ # Push a supplied list of files from the local path into the remote ftp path
128
+ def push_files(localpath, remotepath, filelist)
129
+ connect!
130
+
131
+ remote_paths = filelist.map {|f| File.dirname(f) }.uniq.reject{|p| p == '.' }
132
+ create_remote_paths(remotepath, remote_paths)
133
+
134
+ filelist.each do |f|
135
+ @connection.put File.join(localpath, f), "#{remotepath}/#{f}"
136
+ log "Pushed file #{remotepath}/#{f}"
137
+ end
138
+ close!
139
+ end
140
+
141
+ # Remove listed files from the FTP server
142
+ def remove_files(basepath, filelist)
143
+ connect!
144
+
145
+ filelist.each do |f|
146
+ begin
147
+ @connection.delete "#{basepath}/#{f}".gsub(/\/+/, '/')
148
+ log "Removed file #{basepath}/#{f}"
149
+ rescue Net::FTPPermError => e
150
+ raise e unless /^550/ =~ e.message
151
+ end
152
+ end
153
+
154
+ close!
155
+ end
156
+
157
+ # Chains off to the (if supplied) Ignore class, ie GitIgnores.new.ignore?('path/to/my/file')
158
+ def should_ignore?(path)
159
+ @ignore && @ignore.ignore?(path)
160
+ end
161
+
162
+ private
163
+ def connect!
164
+ @connection = Net::FTP.new(@server)
165
+ @connection.login(@user, @password)
166
+ log "Opened connection to #{@server}"
167
+ end
168
+
169
+ def close!
170
+ @connection.close
171
+ log "Closed Connection to #{@server}"
172
+ end
173
+
174
+ def create_remote_paths(base, pathlist)
175
+ base = '' if base == '/'
176
+ pathlist.each do |remotepath|
177
+ parent = base
178
+ remotepath.split('/').each do |p|
179
+ parent = "#{parent}/#{p}"
180
+ @connection.mkdir(parent) rescue Net::FTPPermError
181
+ log "Creating Remote Directory #{parent}"
182
+ end
183
+ end
184
+ end
185
+
186
+ def log(msg)
187
+ puts msg if @verbose
188
+ end
189
+ end
@@ -0,0 +1,219 @@
1
+ test_path = File.expand_path(File.dirname(__FILE__))
2
+ lib_path = File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib')
3
+
4
+ $:.unshift test_path unless $:.include?(test_path)
5
+ $:.unshift lib_path unless $:.include?(lib_path)
6
+
7
+ require 'rubygems'
8
+ require 'test/unit'
9
+ require 'net/ftp'
10
+ require 'ftp_sync'
11
+ require 'tmpdir'
12
+ require 'fileutils'
13
+
14
+ class Ignore
15
+ def ignore?(p); p == 'ignore' ? true : false; end
16
+ end
17
+
18
+ class FtpSyncTest < Test::Unit::TestCase
19
+
20
+ def setup
21
+ Net::FTP.create_ftp_src
22
+ Net::FTP.listing_overrides = {}
23
+ @local = File.join Dir.tmpdir, create_tmpname
24
+ FileUtils.mkdir_p @local
25
+ @ftp = FtpSync.new('test.server', 'user', 'pass')
26
+ end
27
+
28
+ def teardown
29
+ FileUtils.rm_rf @local
30
+ FileUtils.rm_rf Net::FTP.ftp_src
31
+ FileUtils.rm_rf Net::FTP.ftp_dst if File.exist?(Net::FTP.ftp_dst)
32
+ end
33
+
34
+ def test_can_initialize_with_params
35
+ assert_equal 'test.server', @ftp.server
36
+ assert_equal 'user', @ftp.user
37
+ assert_equal 'pass', @ftp.password
38
+ end
39
+
40
+ def test_can_set_verbose
41
+ @ftp.verbose = true
42
+ assert_equal true, @ftp.verbose
43
+ @ftp.verbose = false
44
+ assert_equal false, @ftp.verbose
45
+ end
46
+
47
+ def test_setting_an_ignore_object
48
+ ftp = FtpSync.new('localhost', 'user', 'pass', { :ignore => Ignore.new })
49
+ assert ftp.should_ignore?('ignore')
50
+ assert !ftp.should_ignore?('something')
51
+ end
52
+
53
+ def test_pulling_from_an_unknown_server
54
+ assert_raise SocketError do
55
+ ftp = FtpSync.new('unknown.server', 'user', 'pass')
56
+ ftp.pull_files(@local, '/', ['README'])
57
+ end
58
+ end
59
+
60
+ def test_pulling_files_with_bad_account_details
61
+ assert_raise Net::FTPPermError do
62
+ ftp = FtpSync.new('test.server', 'unknown', 'unknown')
63
+ ftp.pull_files(@local, '/', ['README'])
64
+ end
65
+ end
66
+
67
+ def test_pulling_files
68
+ @ftp.pull_files(@local, '/', ['README', 'fileA'])
69
+ assert File.exist?(File.join(@local, 'README'))
70
+ assert File.exist?(File.join(@local, 'fileA'))
71
+ end
72
+
73
+ def test_pulling_unknown_files
74
+ assert_raise Net::FTPPermError do
75
+ @ftp.pull_files(@local, '/', ['unknown' ])
76
+ end
77
+ end
78
+
79
+ def test_pulling_files_from_subdirs
80
+ @ftp.pull_files(@local, '/', ['dirA/fileAA'])
81
+ assert File.exist?(File.join(@local, 'dirA/fileAA'))
82
+ end
83
+
84
+ def test_pull_dir_from_root
85
+ @ftp.pull_dir(@local, '/')
86
+ assert File.exist?(File.join(@local, 'fileA'))
87
+ assert File.exist?(File.join(@local, 'fileB'))
88
+ assert File.exist?(File.join(@local, 'dirA/fileAA'))
89
+ assert File.exist?(File.join(@local, 'dirA/dirAA/fileAAA'))
90
+ assert File.exist?(File.join(@local, 'dirB/fileBA'))
91
+ assert File.exist?(File.join(@local, 'dirB/fileBB'))
92
+ end
93
+
94
+ def test_pull_dir_from_subdir
95
+ @ftp.pull_dir(@local, '/dirA')
96
+ assert File.exist?(File.join(@local, 'fileAA'))
97
+ assert File.exist?(File.join(@local, 'dirAA/fileAAA'))
98
+ end
99
+
100
+ def test_pull_dir_from_nonexistant_dir
101
+ assert_raise Net::FTPPermError do
102
+ @ftp.pull_dir(@local, 'something')
103
+ end
104
+ end
105
+
106
+ def test_pulling_dir_over_existing_files
107
+ assert_nothing_raised do
108
+ @ftp.pull_dir(@local, '/')
109
+ FileUtils.rm File.join(@local, 'README')
110
+ @ftp.pull_dir(@local, '/')
111
+ assert File.exist?(File.join(@local, 'README'))
112
+ end
113
+ end
114
+
115
+ def test_pulling_dir_with_deleting_files
116
+ @ftp.pull_dir(@local, '/')
117
+ FileUtils.rm_r File.join(Net::FTP.ftp_src, 'README')
118
+ @ftp.pull_dir(@local, '/', :delete => true)
119
+ assert !File.exist?(File.join(@local, 'README'))
120
+ end
121
+
122
+ def test_pulling_dir_with_not_deleting_files
123
+ @ftp.pull_dir(@local, '/')
124
+ assert File.exist?(File.join(@local, 'README'))
125
+ FileUtils.rm_r File.join(Net::FTP.ftp_src, 'README')
126
+ @ftp.pull_dir(@local, '/')
127
+ assert File.exist?(File.join(@local, 'README'))
128
+ end
129
+
130
+ def test_quick_pull_of_file_older_than_change_date
131
+ @ftp.pull_dir(@local, '/')
132
+ File.open(File.join(Net::FTP.ftp_src, 'README'), 'w') {|f| f.write 'quicktest' }
133
+ Net::FTP.listing_overrides['/'] = ["-rw-r--r-- 1 root other 0 #{(Time.now - 600).strftime('%b %d %H:%M')} README"]
134
+ @ftp.pull_dir(@local, '/', :since => Time.now - 120)
135
+ assert_no_match /quicktest/, File.read(File.join(@local, 'README'))
136
+ end
137
+
138
+ def test_quick_pull_of_file_newer_than_change_date
139
+ @ftp.pull_dir(@local, '/')
140
+ File.open(File.join(Net::FTP.ftp_src, 'README'), 'w') {|f| f.write 'quicktest' }
141
+ Net::FTP.listing_overrides['/'] = ["-rw-r--r-- 1 root other 0 #{(Time.now - 30).strftime('%b %d %H:%M')} README"]
142
+ @ftp.pull_dir(@local, '/', :since => Time.now - 120)
143
+ assert_match /quicktest/, File.read(File.join(@local, 'README'))
144
+ end
145
+
146
+ def test_quick_pull_of_file_older_than_change_date_with_incorrect_file_size
147
+ @ftp.pull_dir(@local, '/')
148
+ File.open(File.join(Net::FTP.ftp_src, 'README'), 'w') {|f| f.write 'quicktest' }
149
+ Net::FTP.listing_overrides['/'] = ["-rw-r--r-- 1 root other 9 #{(Time.now - 600).strftime('%b %d %H:%M')} README"]
150
+ @ftp.pull_dir(@local, '/', :since => Time.now - 120)
151
+ assert_match /quicktest/, File.read(File.join(@local, 'README'))
152
+ end
153
+
154
+ def test_quick_pull_of_file_older_than_dst_file
155
+ @ftp.pull_dir(@local, '/')
156
+ File.open(File.join(Net::FTP.ftp_src, 'README'), 'w') {|f| f.write 'quicktest' }
157
+ Net::FTP.listing_overrides['/'] = ["-rw-r--r-- 1 root other 0 #{(Time.now - 90).strftime('%b %d %H:%M')} README"]
158
+ @ftp.pull_dir(@local, '/', :since => :src)
159
+ assert_no_match /quicktest/, File.read(File.join(@local, 'README'))
160
+ end
161
+
162
+ def test_quick_pull_of_file_newer_than_dst_file
163
+ @ftp.pull_dir(@local, '/')
164
+ File.open(File.join(Net::FTP.ftp_src, 'README'), 'w') {|f| f.write 'quicktest' }
165
+ Net::FTP.listing_overrides['/'] = ["-rw-r--r-- 1 root other 0 #{(Time.now + 90).strftime('%b %d %H:%M')} README"]
166
+ @ftp.pull_dir(@local, '/', :since => :src)
167
+ assert_match /quicktest/, File.read(File.join(@local, 'README'))
168
+ end
169
+
170
+ def test_quick_pull_for_first_download
171
+ @ftp.pull_dir(@local, '/', :since => true)
172
+ assert File.exist?(File.join(@local, 'README'))
173
+ end
174
+
175
+ def test_quick_pull_since_date_for_first_download
176
+ @ftp.pull_dir(@local, '/', :since => Time.now)
177
+ assert File.exist?(File.join(@local, 'README'))
178
+ end
179
+
180
+ def test_pushing_files
181
+ Net::FTP.create_ftp_dst
182
+ FileUtils.touch(File.join(@local, 'localA'))
183
+ FileUtils.mkdir_p(File.join(@local, 'localdirA'))
184
+ FileUtils.touch(File.join(@local, 'localdirA', 'localAA'))
185
+ @ftp.push_files(@local, '/', ['localA', File.join('localdirA', 'localAA')])
186
+ assert File.exist?(File.join(Net::FTP.ftp_dst, 'localA'))
187
+ assert File.exist?(File.join(Net::FTP.ftp_dst, 'localdirA', 'localAA'))
188
+ end
189
+
190
+ def test_pushing_dir
191
+ Net::FTP.create_ftp_dst
192
+ FileUtils.touch(File.join(@local, 'localA'))
193
+ FileUtils.mkdir_p(File.join(@local, 'localdirA'))
194
+ FileUtils.touch(File.join(@local, 'localdirA', 'localAA'))
195
+ @ftp.push_dir(@local, '/')
196
+ assert File.exist?(File.join(Net::FTP.ftp_dst, 'localA'))
197
+ assert File.exist?(File.join(Net::FTP.ftp_dst, 'localdirA', 'localAA'))
198
+ end
199
+
200
+ def test_deleting_files
201
+ Net::FTP.create_ftp_dst
202
+ FileUtils.touch File.join(Net::FTP.ftp_dst, 'fileA')
203
+ FileUtils.mkdir File.join(Net::FTP.ftp_dst, 'dirB')
204
+ FileUtils.touch File.join(Net::FTP.ftp_dst, 'dirB', 'fileB')
205
+ FileUtils.touch File.join(Net::FTP.ftp_dst, 'fileC')
206
+ @ftp.remove_files('/', [ 'fileA', 'dirB/fileB' ])
207
+ assert !File.exist?(File.join(Net::FTP.ftp_dst, 'fileA'))
208
+ assert !File.exist?(File.join(Net::FTP.ftp_dst, 'dirB', 'fileB'))
209
+ assert File.exist?(File.join(Net::FTP.ftp_dst, 'fileC'))
210
+ end
211
+
212
+ protected
213
+ def create_tmpname
214
+ tmpname = ''
215
+ char_list = ("a".."z").to_a + ("0".."9").to_a
216
+ 1.upto(20) { |i| tmpname << char_list[rand(char_list.size)] }
217
+ return tmpname
218
+ end
219
+ end
data/test/net/ftp.rb ADDED
@@ -0,0 +1,117 @@
1
+ require 'tmpdir'
2
+
3
+ module Net
4
+ class FTP
5
+ @@listing_overrides = {}
6
+
7
+ class << self
8
+
9
+ def ftp_src
10
+ @ftp_src ||= File.join(Dir.tmpdir, 'munkey_ftp_src')
11
+ end
12
+
13
+ def ftp_src=(src)
14
+ @ftp_src = src
15
+ end
16
+
17
+ def create_ftp_src
18
+ FileUtils.mkdir_p File.join(ftp_src, 'dirA', 'dirAA')
19
+ FileUtils.mkdir_p File.join(ftp_src, 'dirB')
20
+ FileUtils.touch File.join(ftp_src, 'README')
21
+ FileUtils.touch File.join(ftp_src, 'fileA')
22
+ FileUtils.touch File.join(ftp_src, 'fileB')
23
+ FileUtils.touch File.join(ftp_src, 'dirA', 'fileAA')
24
+ FileUtils.touch File.join(ftp_src, 'dirA', 'dirAA', 'fileAAA')
25
+ FileUtils.touch File.join(ftp_src, 'dirB', 'fileBA')
26
+ FileUtils.touch File.join(ftp_src, 'dirB', 'fileBB')
27
+ end
28
+
29
+ def ftp_dst
30
+ @ftp_dst ||= File.join(Dir.tmpdir, 'munkey_ftp_dst')
31
+ end
32
+
33
+ def ftp_dst=(dst)
34
+ @ftp_dst = dst
35
+ end
36
+
37
+ def create_ftp_dst
38
+ FileUtils.mkdir_p ftp_dst
39
+ end
40
+
41
+ def listing_overrides
42
+ @@listing_overrides ||= {}
43
+ end
44
+
45
+ def listing_overrides=(overrides)
46
+ @@listing_overrides = overrides
47
+ end
48
+ end
49
+
50
+ def initialize(server)
51
+ raise SocketError unless server == 'test.server'
52
+ end
53
+
54
+ def inspect
55
+ "Mocked Net::FTP server=#{@server}"
56
+ end
57
+
58
+ def login(user, pass)
59
+ raise Net::FTPPermError unless user == 'user' && pass == 'pass'
60
+ end
61
+
62
+ def get(src, dst)
63
+ raise Net::FTPPermError unless File.exist?(src_path(src))
64
+ FileUtils.cp src_path(src), dst
65
+ end
66
+
67
+ def put(src, dst)
68
+ d,f = File.split(dst)
69
+ raise Net::FTPPermError unless File.exist?(dst_path(d))
70
+ FileUtils.cp src, dst_path(dst)
71
+ end
72
+
73
+ def mkdir(dir)
74
+ d,sd = File.split(dir)
75
+ raise Net::FTPPermError if File.exist?(dst_path(dir))
76
+ raise Net::FTPPermError unless File.exist?(dst_path(d))
77
+ FileUtils.mkdir dst_path(dir)
78
+ end
79
+
80
+ def chdir(dir)
81
+ raise Net::FTPPermError unless File.exist?(src_path(dir))
82
+ end
83
+
84
+ def list(dir)
85
+ paths = if @@listing_overrides[dir]
86
+ @@listing_overrides[dir]
87
+ elsif File.exist?(src_path(dir))
88
+ `ls -l #{src_path(dir)}`.strip.split("\n")
89
+ else
90
+ []
91
+ end
92
+
93
+ paths.each {|e| yield(e) } if block_given?
94
+ paths
95
+ end
96
+
97
+ def delete(file)
98
+ raise Net::FTPPermError unless File.exist?(dst_path(file))
99
+ File.unlink(dst_path(file))
100
+ end
101
+
102
+ def close; end
103
+
104
+ private
105
+ def src_path(p)
106
+ File.join(self.class.ftp_src, p)
107
+ end
108
+
109
+ def dst_path(p)
110
+ File.join(self.class.ftp_dst, p)
111
+ end
112
+ end
113
+
114
+ class FTPPermError < RuntimeError; end
115
+ end
116
+
117
+ class SocketError < RuntimeError; end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ftp_sync
3
+ version: !ruby/object:Gem::Version
4
+ hash: 13
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 4
9
+ - 1
10
+ version: 0.4.1
11
+ platform: ruby
12
+ authors:
13
+ - jebw
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-12 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: net-ftp-list
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 9
30
+ segments:
31
+ - 2
32
+ - 1
33
+ - 1
34
+ version: 2.1.1
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description: Library for recursively downloading and uploading entire directories from FTP servers. Supports 'quick' downloads pulling only files changed since a specified date and uploading downloading lists of files. Split out from Munkey - a Git <-> FTP tool
38
+ email: jeb@jdwilkins.co.uk
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - README.rdoc
45
+ files:
46
+ - lib/ftp_sync.rb
47
+ - README.rdoc
48
+ - test/ftp_sync_test.rb
49
+ - test/net/ftp.rb
50
+ has_rdoc: true
51
+ homepage: http://github.com/jebw/ftp_sync
52
+ licenses: []
53
+
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --line-numbers
57
+ - --title
58
+ - Library for syncing files and dirs with ftp servers
59
+ - --main
60
+ - README.rdoc
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 3
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 3
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 1.3.7
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: Library for syncing files and dirs with ftp servers
88
+ test_files:
89
+ - test/ftp_sync_test.rb
90
+ - test/net/ftp.rb