ftp_sync 0.4.1

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.
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