]>
Commit | Line | Data |
---|---|---|
4710c53d | 1 | #! /usr/bin/env python\r |
2 | \r | |
3 | """Mirror a remote ftp subtree into a local directory tree.\r | |
4 | \r | |
5 | usage: ftpmirror [-v] [-q] [-i] [-m] [-n] [-r] [-s pat]\r | |
6 | [-l username [-p passwd [-a account]]]\r | |
7 | hostname[:port] [remotedir [localdir]]\r | |
8 | -v: verbose\r | |
9 | -q: quiet\r | |
10 | -i: interactive mode\r | |
11 | -m: macintosh server (NCSA telnet 2.4) (implies -n -s '*.o')\r | |
12 | -n: don't log in\r | |
13 | -r: remove local files/directories no longer pertinent\r | |
14 | -l username [-p passwd [-a account]]: login info (default .netrc or anonymous)\r | |
15 | -s pat: skip files matching pattern\r | |
16 | hostname: remote host w/ optional port separated by ':'\r | |
17 | remotedir: remote directory (default initial)\r | |
18 | localdir: local directory (default current)\r | |
19 | """\r | |
20 | \r | |
21 | import os\r | |
22 | import sys\r | |
23 | import time\r | |
24 | import getopt\r | |
25 | import ftplib\r | |
26 | import netrc\r | |
27 | from fnmatch import fnmatch\r | |
28 | \r | |
29 | # Print usage message and exit\r | |
30 | def usage(*args):\r | |
31 | sys.stdout = sys.stderr\r | |
32 | for msg in args: print msg\r | |
33 | print __doc__\r | |
34 | sys.exit(2)\r | |
35 | \r | |
36 | verbose = 1 # 0 for -q, 2 for -v\r | |
37 | interactive = 0\r | |
38 | mac = 0\r | |
39 | rmok = 0\r | |
40 | nologin = 0\r | |
41 | skippats = ['.', '..', '.mirrorinfo']\r | |
42 | \r | |
43 | # Main program: parse command line and start processing\r | |
44 | def main():\r | |
45 | global verbose, interactive, mac, rmok, nologin\r | |
46 | try:\r | |
47 | opts, args = getopt.getopt(sys.argv[1:], 'a:bil:mnp:qrs:v')\r | |
48 | except getopt.error, msg:\r | |
49 | usage(msg)\r | |
50 | login = ''\r | |
51 | passwd = ''\r | |
52 | account = ''\r | |
53 | if not args: usage('hostname missing')\r | |
54 | host = args[0]\r | |
55 | port = 0\r | |
56 | if ':' in host:\r | |
57 | host, port = host.split(':', 1)\r | |
58 | port = int(port)\r | |
59 | try:\r | |
60 | auth = netrc.netrc().authenticators(host)\r | |
61 | if auth is not None:\r | |
62 | login, account, passwd = auth\r | |
63 | except (netrc.NetrcParseError, IOError):\r | |
64 | pass\r | |
65 | for o, a in opts:\r | |
66 | if o == '-l': login = a\r | |
67 | if o == '-p': passwd = a\r | |
68 | if o == '-a': account = a\r | |
69 | if o == '-v': verbose = verbose + 1\r | |
70 | if o == '-q': verbose = 0\r | |
71 | if o == '-i': interactive = 1\r | |
72 | if o == '-m': mac = 1; nologin = 1; skippats.append('*.o')\r | |
73 | if o == '-n': nologin = 1\r | |
74 | if o == '-r': rmok = 1\r | |
75 | if o == '-s': skippats.append(a)\r | |
76 | remotedir = ''\r | |
77 | localdir = ''\r | |
78 | if args[1:]:\r | |
79 | remotedir = args[1]\r | |
80 | if args[2:]:\r | |
81 | localdir = args[2]\r | |
82 | if args[3:]: usage('too many arguments')\r | |
83 | #\r | |
84 | f = ftplib.FTP()\r | |
85 | if verbose: print "Connecting to '%s%s'..." % (host,\r | |
86 | (port and ":%d"%port or ""))\r | |
87 | f.connect(host,port)\r | |
88 | if not nologin:\r | |
89 | if verbose:\r | |
90 | print 'Logging in as %r...' % (login or 'anonymous')\r | |
91 | f.login(login, passwd, account)\r | |
92 | if verbose: print 'OK.'\r | |
93 | pwd = f.pwd()\r | |
94 | if verbose > 1: print 'PWD =', repr(pwd)\r | |
95 | if remotedir:\r | |
96 | if verbose > 1: print 'cwd(%s)' % repr(remotedir)\r | |
97 | f.cwd(remotedir)\r | |
98 | if verbose > 1: print 'OK.'\r | |
99 | pwd = f.pwd()\r | |
100 | if verbose > 1: print 'PWD =', repr(pwd)\r | |
101 | #\r | |
102 | mirrorsubdir(f, localdir)\r | |
103 | \r | |
104 | # Core logic: mirror one subdirectory (recursively)\r | |
105 | def mirrorsubdir(f, localdir):\r | |
106 | pwd = f.pwd()\r | |
107 | if localdir and not os.path.isdir(localdir):\r | |
108 | if verbose: print 'Creating local directory', repr(localdir)\r | |
109 | try:\r | |
110 | makedir(localdir)\r | |
111 | except os.error, msg:\r | |
112 | print "Failed to establish local directory", repr(localdir)\r | |
113 | return\r | |
114 | infofilename = os.path.join(localdir, '.mirrorinfo')\r | |
115 | try:\r | |
116 | text = open(infofilename, 'r').read()\r | |
117 | except IOError, msg:\r | |
118 | text = '{}'\r | |
119 | try:\r | |
120 | info = eval(text)\r | |
121 | except (SyntaxError, NameError):\r | |
122 | print 'Bad mirror info in', repr(infofilename)\r | |
123 | info = {}\r | |
124 | subdirs = []\r | |
125 | listing = []\r | |
126 | if verbose: print 'Listing remote directory %r...' % (pwd,)\r | |
127 | f.retrlines('LIST', listing.append)\r | |
128 | filesfound = []\r | |
129 | for line in listing:\r | |
130 | if verbose > 1: print '-->', repr(line)\r | |
131 | if mac:\r | |
132 | # Mac listing has just filenames;\r | |
133 | # trailing / means subdirectory\r | |
134 | filename = line.strip()\r | |
135 | mode = '-'\r | |
136 | if filename[-1:] == '/':\r | |
137 | filename = filename[:-1]\r | |
138 | mode = 'd'\r | |
139 | infostuff = ''\r | |
140 | else:\r | |
141 | # Parse, assuming a UNIX listing\r | |
142 | words = line.split(None, 8)\r | |
143 | if len(words) < 6:\r | |
144 | if verbose > 1: print 'Skipping short line'\r | |
145 | continue\r | |
146 | filename = words[-1].lstrip()\r | |
147 | i = filename.find(" -> ")\r | |
148 | if i >= 0:\r | |
149 | # words[0] had better start with 'l'...\r | |
150 | if verbose > 1:\r | |
151 | print 'Found symbolic link %r' % (filename,)\r | |
152 | linkto = filename[i+4:]\r | |
153 | filename = filename[:i]\r | |
154 | infostuff = words[-5:-1]\r | |
155 | mode = words[0]\r | |
156 | skip = 0\r | |
157 | for pat in skippats:\r | |
158 | if fnmatch(filename, pat):\r | |
159 | if verbose > 1:\r | |
160 | print 'Skip pattern', repr(pat),\r | |
161 | print 'matches', repr(filename)\r | |
162 | skip = 1\r | |
163 | break\r | |
164 | if skip:\r | |
165 | continue\r | |
166 | if mode[0] == 'd':\r | |
167 | if verbose > 1:\r | |
168 | print 'Remembering subdirectory', repr(filename)\r | |
169 | subdirs.append(filename)\r | |
170 | continue\r | |
171 | filesfound.append(filename)\r | |
172 | if info.has_key(filename) and info[filename] == infostuff:\r | |
173 | if verbose > 1:\r | |
174 | print 'Already have this version of',repr(filename)\r | |
175 | continue\r | |
176 | fullname = os.path.join(localdir, filename)\r | |
177 | tempname = os.path.join(localdir, '@'+filename)\r | |
178 | if interactive:\r | |
179 | doit = askabout('file', filename, pwd)\r | |
180 | if not doit:\r | |
181 | if not info.has_key(filename):\r | |
182 | info[filename] = 'Not retrieved'\r | |
183 | continue\r | |
184 | try:\r | |
185 | os.unlink(tempname)\r | |
186 | except os.error:\r | |
187 | pass\r | |
188 | if mode[0] == 'l':\r | |
189 | if verbose:\r | |
190 | print "Creating symlink %r -> %r" % (filename, linkto)\r | |
191 | try:\r | |
192 | os.symlink(linkto, tempname)\r | |
193 | except IOError, msg:\r | |
194 | print "Can't create %r: %s" % (tempname, msg)\r | |
195 | continue\r | |
196 | else:\r | |
197 | try:\r | |
198 | fp = open(tempname, 'wb')\r | |
199 | except IOError, msg:\r | |
200 | print "Can't create %r: %s" % (tempname, msg)\r | |
201 | continue\r | |
202 | if verbose:\r | |
203 | print 'Retrieving %r from %r as %r...' % (filename, pwd, fullname)\r | |
204 | if verbose:\r | |
205 | fp1 = LoggingFile(fp, 1024, sys.stdout)\r | |
206 | else:\r | |
207 | fp1 = fp\r | |
208 | t0 = time.time()\r | |
209 | try:\r | |
210 | f.retrbinary('RETR ' + filename,\r | |
211 | fp1.write, 8*1024)\r | |
212 | except ftplib.error_perm, msg:\r | |
213 | print msg\r | |
214 | t1 = time.time()\r | |
215 | bytes = fp.tell()\r | |
216 | fp.close()\r | |
217 | if fp1 != fp:\r | |
218 | fp1.close()\r | |
219 | try:\r | |
220 | os.unlink(fullname)\r | |
221 | except os.error:\r | |
222 | pass # Ignore the error\r | |
223 | try:\r | |
224 | os.rename(tempname, fullname)\r | |
225 | except os.error, msg:\r | |
226 | print "Can't rename %r to %r: %s" % (tempname, fullname, msg)\r | |
227 | continue\r | |
228 | info[filename] = infostuff\r | |
229 | writedict(info, infofilename)\r | |
230 | if verbose and mode[0] != 'l':\r | |
231 | dt = t1 - t0\r | |
232 | kbytes = bytes / 1024.0\r | |
233 | print int(round(kbytes)),\r | |
234 | print 'Kbytes in',\r | |
235 | print int(round(dt)),\r | |
236 | print 'seconds',\r | |
237 | if t1 > t0:\r | |
238 | print '(~%d Kbytes/sec)' % \\r | |
239 | int(round(kbytes/dt),)\r | |
240 | print\r | |
241 | #\r | |
242 | # Remove files from info that are no longer remote\r | |
243 | deletions = 0\r | |
244 | for filename in info.keys():\r | |
245 | if filename not in filesfound:\r | |
246 | if verbose:\r | |
247 | print "Removing obsolete info entry for",\r | |
248 | print repr(filename), "in", repr(localdir or ".")\r | |
249 | del info[filename]\r | |
250 | deletions = deletions + 1\r | |
251 | if deletions:\r | |
252 | writedict(info, infofilename)\r | |
253 | #\r | |
254 | # Remove local files that are no longer in the remote directory\r | |
255 | try:\r | |
256 | if not localdir: names = os.listdir(os.curdir)\r | |
257 | else: names = os.listdir(localdir)\r | |
258 | except os.error:\r | |
259 | names = []\r | |
260 | for name in names:\r | |
261 | if name[0] == '.' or info.has_key(name) or name in subdirs:\r | |
262 | continue\r | |
263 | skip = 0\r | |
264 | for pat in skippats:\r | |
265 | if fnmatch(name, pat):\r | |
266 | if verbose > 1:\r | |
267 | print 'Skip pattern', repr(pat),\r | |
268 | print 'matches', repr(name)\r | |
269 | skip = 1\r | |
270 | break\r | |
271 | if skip:\r | |
272 | continue\r | |
273 | fullname = os.path.join(localdir, name)\r | |
274 | if not rmok:\r | |
275 | if verbose:\r | |
276 | print 'Local file', repr(fullname),\r | |
277 | print 'is no longer pertinent'\r | |
278 | continue\r | |
279 | if verbose: print 'Removing local file/dir', repr(fullname)\r | |
280 | remove(fullname)\r | |
281 | #\r | |
282 | # Recursively mirror subdirectories\r | |
283 | for subdir in subdirs:\r | |
284 | if interactive:\r | |
285 | doit = askabout('subdirectory', subdir, pwd)\r | |
286 | if not doit: continue\r | |
287 | if verbose: print 'Processing subdirectory', repr(subdir)\r | |
288 | localsubdir = os.path.join(localdir, subdir)\r | |
289 | pwd = f.pwd()\r | |
290 | if verbose > 1:\r | |
291 | print 'Remote directory now:', repr(pwd)\r | |
292 | print 'Remote cwd', repr(subdir)\r | |
293 | try:\r | |
294 | f.cwd(subdir)\r | |
295 | except ftplib.error_perm, msg:\r | |
296 | print "Can't chdir to", repr(subdir), ":", repr(msg)\r | |
297 | else:\r | |
298 | if verbose: print 'Mirroring as', repr(localsubdir)\r | |
299 | mirrorsubdir(f, localsubdir)\r | |
300 | if verbose > 1: print 'Remote cwd ..'\r | |
301 | f.cwd('..')\r | |
302 | newpwd = f.pwd()\r | |
303 | if newpwd != pwd:\r | |
304 | print 'Ended up in wrong directory after cd + cd ..'\r | |
305 | print 'Giving up now.'\r | |
306 | break\r | |
307 | else:\r | |
308 | if verbose > 1: print 'OK.'\r | |
309 | \r | |
310 | # Helper to remove a file or directory tree\r | |
311 | def remove(fullname):\r | |
312 | if os.path.isdir(fullname) and not os.path.islink(fullname):\r | |
313 | try:\r | |
314 | names = os.listdir(fullname)\r | |
315 | except os.error:\r | |
316 | names = []\r | |
317 | ok = 1\r | |
318 | for name in names:\r | |
319 | if not remove(os.path.join(fullname, name)):\r | |
320 | ok = 0\r | |
321 | if not ok:\r | |
322 | return 0\r | |
323 | try:\r | |
324 | os.rmdir(fullname)\r | |
325 | except os.error, msg:\r | |
326 | print "Can't remove local directory %r: %s" % (fullname, msg)\r | |
327 | return 0\r | |
328 | else:\r | |
329 | try:\r | |
330 | os.unlink(fullname)\r | |
331 | except os.error, msg:\r | |
332 | print "Can't remove local file %r: %s" % (fullname, msg)\r | |
333 | return 0\r | |
334 | return 1\r | |
335 | \r | |
336 | # Wrapper around a file for writing to write a hash sign every block.\r | |
337 | class LoggingFile:\r | |
338 | def __init__(self, fp, blocksize, outfp):\r | |
339 | self.fp = fp\r | |
340 | self.bytes = 0\r | |
341 | self.hashes = 0\r | |
342 | self.blocksize = blocksize\r | |
343 | self.outfp = outfp\r | |
344 | def write(self, data):\r | |
345 | self.bytes = self.bytes + len(data)\r | |
346 | hashes = int(self.bytes) / self.blocksize\r | |
347 | while hashes > self.hashes:\r | |
348 | self.outfp.write('#')\r | |
349 | self.outfp.flush()\r | |
350 | self.hashes = self.hashes + 1\r | |
351 | self.fp.write(data)\r | |
352 | def close(self):\r | |
353 | self.outfp.write('\n')\r | |
354 | \r | |
355 | # Ask permission to download a file.\r | |
356 | def askabout(filetype, filename, pwd):\r | |
357 | prompt = 'Retrieve %s %s from %s ? [ny] ' % (filetype, filename, pwd)\r | |
358 | while 1:\r | |
359 | reply = raw_input(prompt).strip().lower()\r | |
360 | if reply in ['y', 'ye', 'yes']:\r | |
361 | return 1\r | |
362 | if reply in ['', 'n', 'no', 'nop', 'nope']:\r | |
363 | return 0\r | |
364 | print 'Please answer yes or no.'\r | |
365 | \r | |
366 | # Create a directory if it doesn't exist. Recursively create the\r | |
367 | # parent directory as well if needed.\r | |
368 | def makedir(pathname):\r | |
369 | if os.path.isdir(pathname):\r | |
370 | return\r | |
371 | dirname = os.path.dirname(pathname)\r | |
372 | if dirname: makedir(dirname)\r | |
373 | os.mkdir(pathname, 0777)\r | |
374 | \r | |
375 | # Write a dictionary to a file in a way that can be read back using\r | |
376 | # rval() but is still somewhat readable (i.e. not a single long line).\r | |
377 | # Also creates a backup file.\r | |
378 | def writedict(dict, filename):\r | |
379 | dir, fname = os.path.split(filename)\r | |
380 | tempname = os.path.join(dir, '@' + fname)\r | |
381 | backup = os.path.join(dir, fname + '~')\r | |
382 | try:\r | |
383 | os.unlink(backup)\r | |
384 | except os.error:\r | |
385 | pass\r | |
386 | fp = open(tempname, 'w')\r | |
387 | fp.write('{\n')\r | |
388 | for key, value in dict.items():\r | |
389 | fp.write('%r: %r,\n' % (key, value))\r | |
390 | fp.write('}\n')\r | |
391 | fp.close()\r | |
392 | try:\r | |
393 | os.rename(filename, backup)\r | |
394 | except os.error:\r | |
395 | pass\r | |
396 | os.rename(tempname, filename)\r | |
397 | \r | |
398 | \r | |
399 | if __name__ == '__main__':\r | |
400 | main()\r |