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 .netrc or anonymous)
15 -s pat: skip files matching pattern
17 remotedir: remote directory (default initial)
18 localdir: local directory (default current)
28 from fnmatch
import fnmatch
30 # Print usage message and exit
32 sys
.stdout
= sys
.stderr
33 for msg
in args
: print msg
37 verbose
= 1 # 0 for -q, 2 for -v
42 skippats
= ['.', '..', '.mirrorinfo']
44 # Main program: parse command line and start processing
46 global verbose
, interactive
, mac
, rmok
, nologin
48 opts
, args
= getopt
.getopt(sys
.argv
[1:], 'a:bil:mnp:qrs:v')
49 except getopt
.error
, msg
:
54 if not args
: usage('hostname missing')
57 auth
= netrc
.netrc().authenticators(host
)
59 login
, account
, passwd
= auth
60 except (netrc
.NetrcParseError
, IOError):
63 if o
== '-l': login
= a
64 if o
== '-p': passwd
= a
65 if o
== '-a': account
= a
66 if o
== '-v': verbose
= verbose
+ 1
67 if o
== '-q': verbose
= 0
68 if o
== '-i': interactive
= 1
69 if o
== '-m': mac
= 1; nologin
= 1; skippats
.append('*.o')
70 if o
== '-n': nologin
= 1
71 if o
== '-r': rmok
= 1
72 if o
== '-s': skippats
.append(a
)
79 if args
[3:]: usage('too many arguments')
82 if verbose
: print 'Connecting to %s...' % `host`
86 print 'Logging in as %s...' % `login
or 'anonymous'`
87 f
.login(login
, passwd
, account
)
88 if verbose
: print 'OK.'
90 if verbose
> 1: print 'PWD =', `pwd`
92 if verbose
> 1: print 'cwd(%s)' % `remotedir`
94 if verbose
> 1: print 'OK.'
96 if verbose
> 1: print 'PWD =', `pwd`
98 mirrorsubdir(f
, localdir
)
100 # Core logic: mirror one subdirectory (recursively)
101 def mirrorsubdir(f
, localdir
):
103 if localdir
and not os
.path
.isdir(localdir
):
104 if verbose
: print 'Creating local directory', `localdir`
107 except os
.error
, msg
:
108 print "Failed to establish local directory", `localdir`
110 infofilename
= os
.path
.join(localdir
, '.mirrorinfo')
112 text
= open(infofilename
, 'r').read()
117 except (SyntaxError, NameError):
118 print 'Bad mirror info in %s' % `infofilename`
122 if verbose
: print 'Listing remote directory %s...' % `pwd`
123 f
.retrlines('LIST', listing
.append
)
126 if verbose
> 1: print '-->', `line`
128 # Mac listing has just filenames;
129 # trailing / means subdirectory
130 filename
= string
.strip(line
)
132 if filename
[-1:] == '/':
133 filename
= filename
[:-1]
137 # Parse, assuming a UNIX listing
138 words
= string
.split(line
, None, 8)
140 if verbose
> 1: print 'Skipping short line'
142 filename
= string
.lstrip(words
[-1])
143 i
= string
.find(filename
, " -> ")
145 # words[0] had better start with 'l'...
147 print 'Found symbolic link %s' % `filename`
148 linkto
= filename
[i
+4:]
149 filename
= filename
[:i
]
150 infostuff
= words
[-5:-1]
154 if fnmatch(filename
, pat
):
156 print 'Skip pattern', `pat`
,
157 print 'matches', `filename`
164 print 'Remembering subdirectory', `filename`
165 subdirs
.append(filename
)
167 filesfound
.append(filename
)
168 if info
.has_key(filename
) and info
[filename
] == infostuff
:
170 print 'Already have this version of',`filename`
172 fullname
= os
.path
.join(localdir
, filename
)
173 tempname
= os
.path
.join(localdir
, '@'+filename
)
175 doit
= askabout('file', filename
, pwd
)
177 if not info
.has_key(filename
):
178 info
[filename
] = 'Not retrieved'
186 print "Creating symlink %s -> %s" % (
187 `filename`
, `linkto`
)
189 os
.symlink(linkto
, tempname
)
191 print "Can't create %s: %s" % (
192 `tempname`
, str(msg
))
196 fp
= open(tempname
, 'wb')
198 print "Can't create %s: %s" % (
199 `tempname`
, str(msg
))
202 print 'Retrieving %s from %s as %s...' % \
203 (`filename`
, `pwd`
, `fullname`
)
205 fp1
= LoggingFile(fp
, 1024, sys
.stdout
)
210 f
.retrbinary('RETR ' + filename
,
212 except ftplib
.error_perm
, msg
:
222 pass # Ignore the error
224 os
.rename(tempname
, fullname
)
225 except os
.error
, msg
:
226 print "Can't rename %s to %s: %s" % (`tempname`
,
230 info
[filename
] = infostuff
231 writedict(info
, infofilename
)
232 if verbose
and mode
[0] != 'l':
234 kbytes
= bytes
/ 1024.0
235 print int(round(kbytes
)),
237 print int(round(dt
)),
240 print '(~%d Kbytes/sec)' % \
241 int(round(kbytes
/dt
),)
244 # Remove files from info that are no longer remote
246 for filename
in info
.keys():
247 if filename
not in filesfound
:
249 print "Removing obsolete info entry for",
250 print `filename`
, "in", `localdir
or "."`
252 deletions
= deletions
+ 1
254 writedict(info
, infofilename
)
256 # Remove local files that are no longer in the remote directory
258 if not localdir
: names
= os
.listdir(os
.curdir
)
259 else: names
= os
.listdir(localdir
)
263 if name
[0] == '.' or info
.has_key(name
) or name
in subdirs
:
267 if fnmatch(name
, pat
):
269 print 'Skip pattern', `pat`
,
270 print 'matches', `name`
275 fullname
= os
.path
.join(localdir
, name
)
278 print 'Local file', `fullname`
,
279 print 'is no longer pertinent'
281 if verbose
: print 'Removing local file/dir', `fullname`
284 # Recursively mirror subdirectories
285 for subdir
in subdirs
:
287 doit
= askabout('subdirectory', subdir
, pwd
)
288 if not doit
: continue
289 if verbose
: print 'Processing subdirectory', `subdir`
290 localsubdir
= os
.path
.join(localdir
, subdir
)
293 print 'Remote directory now:', `pwd`
294 print 'Remote cwd', `subdir`
297 except ftplib
.error_perm
, msg
:
298 print "Can't chdir to", `subdir`
, ":", `msg`
300 if verbose
: print 'Mirroring as', `localsubdir`
301 mirrorsubdir(f
, localsubdir
)
302 if verbose
> 1: print 'Remote cwd ..'
306 print 'Ended up in wrong directory after cd + cd ..'
307 print 'Giving up now.'
310 if verbose
> 1: print 'OK.'
312 # Helper to remove a file or directory tree
313 def remove(fullname
):
314 if os
.path
.isdir(fullname
) and not os
.path
.islink(fullname
):
316 names
= os
.listdir(fullname
)
321 if not remove(os
.path
.join(fullname
, name
)):
327 except os
.error
, msg
:
328 print "Can't remove local directory %s: %s" % \
329 (`fullname`
, str(msg
))
334 except os
.error
, msg
:
335 print "Can't remove local file %s: %s" % \
336 (`fullname`
, str(msg
))
340 # Wrapper around a file for writing to write a hash sign every block.
342 def __init__(self
, fp
, blocksize
, outfp
):
346 self
.blocksize
= blocksize
348 def write(self
, data
):
349 self
.bytes
= self
.bytes
+ len(data
)
350 hashes
= int(self
.bytes
) / self
.blocksize
351 while hashes
> self
.hashes
:
352 self
.outfp
.write('#')
354 self
.hashes
= self
.hashes
+ 1
357 self
.outfp
.write('\n')
359 # Ask permission to download a file.
360 def askabout(filetype
, filename
, pwd
):
361 prompt
= 'Retrieve %s %s from %s ? [ny] ' % (filetype
, filename
, pwd
)
363 reply
= string
.lower(string
.strip(raw_input(prompt
)))
364 if reply
in ['y', 'ye', 'yes']:
366 if reply
in ['', 'n', 'no', 'nop', 'nope']:
368 print 'Please answer yes or no.'
370 # Create a directory if it doesn't exist. Recursively create the
371 # parent directory as well if needed.
372 def makedir(pathname
):
373 if os
.path
.isdir(pathname
):
375 dirname
= os
.path
.dirname(pathname
)
376 if dirname
: makedir(dirname
)
377 os
.mkdir(pathname
, 0777)
379 # Write a dictionary to a file in a way that can be read back using
380 # rval() but is still somewhat readable (i.e. not a single long line).
381 # Also creates a backup file.
382 def writedict(dict, filename
):
383 dir, file = os
.path
.split(filename
)
384 tempname
= os
.path
.join(dir, '@' + file)
385 backup
= os
.path
.join(dir, file + '~')
390 fp
= open(tempname
, 'w')
392 for key
, value
in dict.items():
393 fp
.write('%s: %s,\n' % (`key`
, `value`
))
397 os
.rename(filename
, backup
)
400 os
.rename(tempname
, filename
)
403 if __name__
== '__main__':