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