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 +24 -0
- data/lib/ftp_sync.rb +189 -0
- data/test/ftp_sync_test.rb +219 -0
- data/test/net/ftp.rb +117 -0
- metadata +90 -0
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
|