3 """Mirror a remote ftp subtree into a local directory tree.
5 usage: ftpmirror [-v] [-q] [-i] [-m] [-n] [-r] [-s pat]
6 [-l username [-p passwd [-a account]]]
7 hostname [remotedir [localdir]]
11 -m: macintosh server (NCSA telnet 2.4) (implies -n -s '*.o')
13 -r: remove local files/directories no longer pertinent
14 -l username [-p passwd [-a account]]: login info (default anonymous ftp)
15 -s pat: skip files matching pattern
17 remotedir: remote directory (default initial)
18 localdir: local directory (default current)
27 from fnmatch
import fnmatch
29 # Print usage message and exit
31 sys
.stdout
= sys
.stderr
32 for msg
in args
: print msg
36 verbose
= 1 # 0 for -q, 2 for -v
41 skippats
= ['.', '..', '.mirrorinfo']
43 # Main program: parse command line and start processing
45 global verbose
, interactive
, mac
, rmok
, nologin
47 opts
, args
= getopt
.getopt(sys
.argv
[1:], 'a:bil:mnp:qrs:v')
48 except getopt
.error
, msg
:
54 if o
== '-l': login
= a
55 if o
== '-p': passwd
= a
56 if o
== '-a': account
= a
57 if o
== '-v': verbose
= verbose
+ 1
58 if o
== '-q': verbose
= 0
59 if o
== '-i': interactive
= 1
60 if o
== '-m': mac
= 1; nologin
= 1; skippats
.append('*.o')
61 if o
== '-n': nologin
= 1
62 if o
== '-r': rmok
= 1
63 if o
== '-s': skippats
.append(a
)
64 if not args
: usage('hostname missing')
72 if args
[3:]: usage('too many arguments')
75 if verbose
: print 'Connecting to %s...' % `host`
79 print 'Logging in as %s...' % `login
or 'anonymous'`
80 f
.login(login
, passwd
, account
)
81 if verbose
: print 'OK.'
83 if verbose
> 1: print 'PWD =', `pwd`
85 if verbose
> 1: print 'cwd(%s)' % `remotedir`
87 if verbose
> 1: print 'OK.'
89 if verbose
> 1: print 'PWD =', `pwd`
91 mirrorsubdir(f
, localdir
)
93 # Core logic: mirror one subdirectory (recursively)
94 def mirrorsubdir(f
, localdir
):
96 if localdir
and not os
.path
.isdir(localdir
):
97 if verbose
: print 'Creating local directory', `localdir`
100 except os
.error
, msg
:
101 print "Failed to establish local directory", `localdir`
103 infofilename
= os
.path
.join(localdir
, '.mirrorinfo')
105 text
= open(infofilename
, 'r').read()
110 except (SyntaxError, NameError):
111 print 'Bad mirror info in %s' % `infofilename`
115 if verbose
: print 'Listing remote directory %s...' % `pwd`
116 f
.retrlines('LIST', listing
.append
)
119 if verbose
> 1: print '-->', `line`
121 # Mac listing has just filenames;
122 # trailing / means subdirectory
123 filename
= string
.strip(line
)
125 if filename
[-1:] == '/':
126 filename
= filename
[:-1]
130 # Parse, assuming a UNIX listing
131 words
= string
.split(line
, None, 8)
133 if verbose
> 1: print 'Skipping short line'
135 filename
= string
.lstrip(words
[-1])
136 i
= string
.find(filename
, " -> ")
138 # words[0] had better start with 'l'...
140 print 'Found symbolic link %s' % `filename`
141 linkto
= filename
[i
+4:]
142 filename
= filename
[:i
]
143 infostuff
= words
[-5:-1]
147 if fnmatch(filename
, pat
):
149 print 'Skip pattern', `pat`
,
150 print 'matches', `filename`
157 print 'Remembering subdirectory', `filename`
158 subdirs
.append(filename
)
160 filesfound
.append(filename
)
161 if info
.has_key(filename
) and info
[filename
] == infostuff
:
163 print 'Already have this version of',`filename`
165 fullname
= os
.path
.join(localdir
, filename
)
166 tempname
= os
.path
.join(localdir
, '@'+filename
)
168 doit
= askabout('file', filename
, pwd
)
170 if not info
.has_key(filename
):
171 info
[filename
] = 'Not retrieved'
179 print "Creating symlink %s -> %s" % (
180 `filename`
, `linkto`
)
182 os
.symlink(linkto
, tempname
)
184 print "Can't create %s: %s" % (
185 `tempname`
, str(msg
))
189 fp
= open(tempname
, 'wb')
191 print "Can't create %s: %s" % (
192 `tempname`
, str(msg
))
195 print 'Retrieving %s from %s as %s...' % \
196 (`filename`
, `pwd`
, `fullname`
)
198 fp1
= LoggingFile(fp
, 1024, sys
.stdout
)
203 f
.retrbinary('RETR ' + filename
,
205 except ftplib
.error_perm
, msg
:
215 pass # Ignore the error
217 os
.rename(tempname
, fullname
)
218 except os
.error
, msg
:
219 print "Can't rename %s to %s: %s" % (`tempname`
,
223 info
[filename
] = infostuff
224 writedict(info
, infofilename
)
225 if verbose
and mode
[0] != 'l':
227 kbytes
= bytes
/ 1024.0
228 print int(round(kbytes
)),
230 print int(round(dt
)),
233 print '(~%d Kbytes/sec)' % \
234 int(round(kbytes
/dt
),)
237 # Remove files from info that are no longer remote
239 for filename
in info
.keys():
240 if filename
not in filesfound
:
242 print "Removing obsolete info entry for",
243 print `filename`
, "in", `localdir
or "."`
245 deletions
= deletions
+ 1
247 writedict(info
, infofilename
)
249 # Remove local files that are no longer in the remote directory
251 if not localdir
: names
= os
.listdir(os
.curdir
)
252 else: names
= os
.listdir(localdir
)
256 if name
[0] == '.' or info
.has_key(name
) or name
in subdirs
:
260 if fnmatch(name
, pat
):
262 print 'Skip pattern', `pat`
,
263 print 'matches', `name`
268 fullname
= os
.path
.join(localdir
, name
)
271 print 'Local file', `fullname`
,
272 print 'is no longer pertinent'
274 if verbose
: print 'Removing local file/dir', `fullname`
277 # Recursively mirror subdirectories
278 for subdir
in subdirs
:
280 doit
= askabout('subdirectory', subdir
, pwd
)
281 if not doit
: continue
282 if verbose
: print 'Processing subdirectory', `subdir`
283 localsubdir
= os
.path
.join(localdir
, subdir
)
286 print 'Remote directory now:', `pwd`
287 print 'Remote cwd', `subdir`
290 except ftplib
.error_perm
, msg
:
291 print "Can't chdir to", `subdir`
, ":", `msg`
293 if verbose
: print 'Mirroring as', `localsubdir`
294 mirrorsubdir(f
, localsubdir
)
295 if verbose
> 1: print 'Remote cwd ..'
299 print 'Ended up in wrong directory after cd + cd ..'
300 print 'Giving up now.'
303 if verbose
> 1: print 'OK.'
305 # Helper to remove a file or directory tree
306 def remove(fullname
):
307 if os
.path
.isdir(fullname
) and not os
.path
.islink(fullname
):
309 names
= os
.listdir(fullname
)
314 if not remove(os
.path
.join(fullname
, name
)):
320 except os
.error
, msg
:
321 print "Can't remove local directory %s: %s" % \
322 (`fullname`
, str(msg
))
327 except os
.error
, msg
:
328 print "Can't remove local file %s: %s" % \
329 (`fullname`
, str(msg
))
333 # Wrapper around a file for writing to write a hash sign every block.
335 def __init__(self
, fp
, blocksize
, outfp
):
339 self
.blocksize
= blocksize
341 def write(self
, data
):
342 self
.bytes
= self
.bytes
+ len(data
)
343 hashes
= int(self
.bytes
) / self
.blocksize
344 while hashes
> self
.hashes
:
345 self
.outfp
.write('#')
347 self
.hashes
= self
.hashes
+ 1
350 self
.outfp
.write('\n')
352 # Ask permission to download a file.
353 def askabout(filetype
, filename
, pwd
):
354 prompt
= 'Retrieve %s %s from %s ? [ny] ' % (filetype
, filename
, pwd
)
356 reply
= string
.lower(string
.strip(raw_input(prompt
)))
357 if reply
in ['y', 'ye', 'yes']:
359 if reply
in ['', 'n', 'no', 'nop', 'nope']:
361 print 'Please answer yes or no.'
363 # Create a directory if it doesn't exist. Recursively create the
364 # parent directory as well if needed.
365 def makedir(pathname
):
366 if os
.path
.isdir(pathname
):
368 dirname
= os
.path
.dirname(pathname
)
369 if dirname
: makedir(dirname
)
370 os
.mkdir(pathname
, 0777)
372 # Write a dictionary to a file in a way that can be read back using
373 # rval() but is still somewhat readable (i.e. not a single long line).
374 # Also creates a backup file.
375 def writedict(dict, filename
):
376 dir, file = os
.path
.split(filename
)
377 tempname
= os
.path
.join(dir, '@' + file)
378 backup
= os
.path
.join(dir, file + '~')
383 fp
= open(tempname
, 'w')
385 for key
, value
in dict.items():
386 fp
.write('%s: %s,\n' % (`key`
, `value`
))
390 os
.rename(filename
, backup
)
393 os
.rename(tempname
, filename
)