##// END OF EJS Templates
byterange: replace uses of hasattr with getattr
Augie Fackler -
r14947:3aa34005 default
parent child Browse files
Show More
@@ -1,466 +1,462
1 # This library is free software; you can redistribute it and/or
1 # This library is free software; you can redistribute it and/or
2 # modify it under the terms of the GNU Lesser General Public
2 # modify it under the terms of the GNU Lesser General Public
3 # License as published by the Free Software Foundation; either
3 # License as published by the Free Software Foundation; either
4 # version 2.1 of the License, or (at your option) any later version.
4 # version 2.1 of the License, or (at your option) any later version.
5 #
5 #
6 # This library is distributed in the hope that it will be useful,
6 # This library is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 # Lesser General Public License for more details.
9 # Lesser General Public License for more details.
10 #
10 #
11 # You should have received a copy of the GNU Lesser General Public
11 # You should have received a copy of the GNU Lesser General Public
12 # License along with this library; if not, write to the
12 # License along with this library; if not, write to the
13 # Free Software Foundation, Inc.,
13 # Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330,
14 # 59 Temple Place, Suite 330,
15 # Boston, MA 02111-1307 USA
15 # Boston, MA 02111-1307 USA
16
16
17 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
17 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
18 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
18 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
19
19
20 # $Id: byterange.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $
20 # $Id: byterange.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $
21
21
22 import os
22 import os
23 import stat
23 import stat
24 import urllib
24 import urllib
25 import urllib2
25 import urllib2
26 import email.Utils
26 import email.Utils
27
27
28 class RangeError(IOError):
28 class RangeError(IOError):
29 """Error raised when an unsatisfiable range is requested."""
29 """Error raised when an unsatisfiable range is requested."""
30 pass
30 pass
31
31
32 class HTTPRangeHandler(urllib2.BaseHandler):
32 class HTTPRangeHandler(urllib2.BaseHandler):
33 """Handler that enables HTTP Range headers.
33 """Handler that enables HTTP Range headers.
34
34
35 This was extremely simple. The Range header is a HTTP feature to
35 This was extremely simple. The Range header is a HTTP feature to
36 begin with so all this class does is tell urllib2 that the
36 begin with so all this class does is tell urllib2 that the
37 "206 Partial Content" reponse from the HTTP server is what we
37 "206 Partial Content" reponse from the HTTP server is what we
38 expected.
38 expected.
39
39
40 Example:
40 Example:
41 import urllib2
41 import urllib2
42 import byterange
42 import byterange
43
43
44 range_handler = range.HTTPRangeHandler()
44 range_handler = range.HTTPRangeHandler()
45 opener = urllib2.build_opener(range_handler)
45 opener = urllib2.build_opener(range_handler)
46
46
47 # install it
47 # install it
48 urllib2.install_opener(opener)
48 urllib2.install_opener(opener)
49
49
50 # create Request and set Range header
50 # create Request and set Range header
51 req = urllib2.Request('http://www.python.org/')
51 req = urllib2.Request('http://www.python.org/')
52 req.header['Range'] = 'bytes=30-50'
52 req.header['Range'] = 'bytes=30-50'
53 f = urllib2.urlopen(req)
53 f = urllib2.urlopen(req)
54 """
54 """
55
55
56 def http_error_206(self, req, fp, code, msg, hdrs):
56 def http_error_206(self, req, fp, code, msg, hdrs):
57 # 206 Partial Content Response
57 # 206 Partial Content Response
58 r = urllib.addinfourl(fp, hdrs, req.get_full_url())
58 r = urllib.addinfourl(fp, hdrs, req.get_full_url())
59 r.code = code
59 r.code = code
60 r.msg = msg
60 r.msg = msg
61 return r
61 return r
62
62
63 def http_error_416(self, req, fp, code, msg, hdrs):
63 def http_error_416(self, req, fp, code, msg, hdrs):
64 # HTTP's Range Not Satisfiable error
64 # HTTP's Range Not Satisfiable error
65 raise RangeError('Requested Range Not Satisfiable')
65 raise RangeError('Requested Range Not Satisfiable')
66
66
67 class RangeableFileObject(object):
67 class RangeableFileObject(object):
68 """File object wrapper to enable raw range handling.
68 """File object wrapper to enable raw range handling.
69 This was implemented primarilary for handling range
69 This was implemented primarilary for handling range
70 specifications for file:// urls. This object effectively makes
70 specifications for file:// urls. This object effectively makes
71 a file object look like it consists only of a range of bytes in
71 a file object look like it consists only of a range of bytes in
72 the stream.
72 the stream.
73
73
74 Examples:
74 Examples:
75 # expose 10 bytes, starting at byte position 20, from
75 # expose 10 bytes, starting at byte position 20, from
76 # /etc/aliases.
76 # /etc/aliases.
77 >>> fo = RangeableFileObject(file('/etc/passwd', 'r'), (20,30))
77 >>> fo = RangeableFileObject(file('/etc/passwd', 'r'), (20,30))
78 # seek seeks within the range (to position 23 in this case)
78 # seek seeks within the range (to position 23 in this case)
79 >>> fo.seek(3)
79 >>> fo.seek(3)
80 # tell tells where your at _within the range_ (position 3 in
80 # tell tells where your at _within the range_ (position 3 in
81 # this case)
81 # this case)
82 >>> fo.tell()
82 >>> fo.tell()
83 # read EOFs if an attempt is made to read past the last
83 # read EOFs if an attempt is made to read past the last
84 # byte in the range. the following will return only 7 bytes.
84 # byte in the range. the following will return only 7 bytes.
85 >>> fo.read(30)
85 >>> fo.read(30)
86 """
86 """
87
87
88 def __init__(self, fo, rangetup):
88 def __init__(self, fo, rangetup):
89 """Create a RangeableFileObject.
89 """Create a RangeableFileObject.
90 fo -- a file like object. only the read() method need be
90 fo -- a file like object. only the read() method need be
91 supported but supporting an optimized seek() is
91 supported but supporting an optimized seek() is
92 preferable.
92 preferable.
93 rangetup -- a (firstbyte,lastbyte) tuple specifying the range
93 rangetup -- a (firstbyte,lastbyte) tuple specifying the range
94 to work over.
94 to work over.
95 The file object provided is assumed to be at byte offset 0.
95 The file object provided is assumed to be at byte offset 0.
96 """
96 """
97 self.fo = fo
97 self.fo = fo
98 (self.firstbyte, self.lastbyte) = range_tuple_normalize(rangetup)
98 (self.firstbyte, self.lastbyte) = range_tuple_normalize(rangetup)
99 self.realpos = 0
99 self.realpos = 0
100 self._do_seek(self.firstbyte)
100 self._do_seek(self.firstbyte)
101
101
102 def __getattr__(self, name):
102 def __getattr__(self, name):
103 """This effectively allows us to wrap at the instance level.
103 """This effectively allows us to wrap at the instance level.
104 Any attribute not found in _this_ object will be searched for
104 Any attribute not found in _this_ object will be searched for
105 in self.fo. This includes methods."""
105 in self.fo. This includes methods."""
106 if hasattr(self.fo, name):
107 return getattr(self.fo, name)
106 return getattr(self.fo, name)
108 raise AttributeError(name)
109
107
110 def tell(self):
108 def tell(self):
111 """Return the position within the range.
109 """Return the position within the range.
112 This is different from fo.seek in that position 0 is the
110 This is different from fo.seek in that position 0 is the
113 first byte position of the range tuple. For example, if
111 first byte position of the range tuple. For example, if
114 this object was created with a range tuple of (500,899),
112 this object was created with a range tuple of (500,899),
115 tell() will return 0 when at byte position 500 of the file.
113 tell() will return 0 when at byte position 500 of the file.
116 """
114 """
117 return (self.realpos - self.firstbyte)
115 return (self.realpos - self.firstbyte)
118
116
119 def seek(self, offset, whence=0):
117 def seek(self, offset, whence=0):
120 """Seek within the byte range.
118 """Seek within the byte range.
121 Positioning is identical to that described under tell().
119 Positioning is identical to that described under tell().
122 """
120 """
123 assert whence in (0, 1, 2)
121 assert whence in (0, 1, 2)
124 if whence == 0: # absolute seek
122 if whence == 0: # absolute seek
125 realoffset = self.firstbyte + offset
123 realoffset = self.firstbyte + offset
126 elif whence == 1: # relative seek
124 elif whence == 1: # relative seek
127 realoffset = self.realpos + offset
125 realoffset = self.realpos + offset
128 elif whence == 2: # absolute from end of file
126 elif whence == 2: # absolute from end of file
129 # XXX: are we raising the right Error here?
127 # XXX: are we raising the right Error here?
130 raise IOError('seek from end of file not supported.')
128 raise IOError('seek from end of file not supported.')
131
129
132 # do not allow seek past lastbyte in range
130 # do not allow seek past lastbyte in range
133 if self.lastbyte and (realoffset >= self.lastbyte):
131 if self.lastbyte and (realoffset >= self.lastbyte):
134 realoffset = self.lastbyte
132 realoffset = self.lastbyte
135
133
136 self._do_seek(realoffset - self.realpos)
134 self._do_seek(realoffset - self.realpos)
137
135
138 def read(self, size=-1):
136 def read(self, size=-1):
139 """Read within the range.
137 """Read within the range.
140 This method will limit the size read based on the range.
138 This method will limit the size read based on the range.
141 """
139 """
142 size = self._calc_read_size(size)
140 size = self._calc_read_size(size)
143 rslt = self.fo.read(size)
141 rslt = self.fo.read(size)
144 self.realpos += len(rslt)
142 self.realpos += len(rslt)
145 return rslt
143 return rslt
146
144
147 def readline(self, size=-1):
145 def readline(self, size=-1):
148 """Read lines within the range.
146 """Read lines within the range.
149 This method will limit the size read based on the range.
147 This method will limit the size read based on the range.
150 """
148 """
151 size = self._calc_read_size(size)
149 size = self._calc_read_size(size)
152 rslt = self.fo.readline(size)
150 rslt = self.fo.readline(size)
153 self.realpos += len(rslt)
151 self.realpos += len(rslt)
154 return rslt
152 return rslt
155
153
156 def _calc_read_size(self, size):
154 def _calc_read_size(self, size):
157 """Handles calculating the amount of data to read based on
155 """Handles calculating the amount of data to read based on
158 the range.
156 the range.
159 """
157 """
160 if self.lastbyte:
158 if self.lastbyte:
161 if size > -1:
159 if size > -1:
162 if ((self.realpos + size) >= self.lastbyte):
160 if ((self.realpos + size) >= self.lastbyte):
163 size = (self.lastbyte - self.realpos)
161 size = (self.lastbyte - self.realpos)
164 else:
162 else:
165 size = (self.lastbyte - self.realpos)
163 size = (self.lastbyte - self.realpos)
166 return size
164 return size
167
165
168 def _do_seek(self, offset):
166 def _do_seek(self, offset):
169 """Seek based on whether wrapped object supports seek().
167 """Seek based on whether wrapped object supports seek().
170 offset is relative to the current position (self.realpos).
168 offset is relative to the current position (self.realpos).
171 """
169 """
172 assert offset >= 0
170 assert offset >= 0
173 if not hasattr(self.fo, 'seek'):
171 seek = getattr(self.fo, 'seek', self._poor_mans_seek)
174 self._poor_mans_seek(offset)
172 seek(self.realpos + offset)
175 else:
176 self.fo.seek(self.realpos + offset)
177 self.realpos += offset
173 self.realpos += offset
178
174
179 def _poor_mans_seek(self, offset):
175 def _poor_mans_seek(self, offset):
180 """Seek by calling the wrapped file objects read() method.
176 """Seek by calling the wrapped file objects read() method.
181 This is used for file like objects that do not have native
177 This is used for file like objects that do not have native
182 seek support. The wrapped objects read() method is called
178 seek support. The wrapped objects read() method is called
183 to manually seek to the desired position.
179 to manually seek to the desired position.
184 offset -- read this number of bytes from the wrapped
180 offset -- read this number of bytes from the wrapped
185 file object.
181 file object.
186 raise RangeError if we encounter EOF before reaching the
182 raise RangeError if we encounter EOF before reaching the
187 specified offset.
183 specified offset.
188 """
184 """
189 pos = 0
185 pos = 0
190 bufsize = 1024
186 bufsize = 1024
191 while pos < offset:
187 while pos < offset:
192 if (pos + bufsize) > offset:
188 if (pos + bufsize) > offset:
193 bufsize = offset - pos
189 bufsize = offset - pos
194 buf = self.fo.read(bufsize)
190 buf = self.fo.read(bufsize)
195 if len(buf) != bufsize:
191 if len(buf) != bufsize:
196 raise RangeError('Requested Range Not Satisfiable')
192 raise RangeError('Requested Range Not Satisfiable')
197 pos += bufsize
193 pos += bufsize
198
194
199 class FileRangeHandler(urllib2.FileHandler):
195 class FileRangeHandler(urllib2.FileHandler):
200 """FileHandler subclass that adds Range support.
196 """FileHandler subclass that adds Range support.
201 This class handles Range headers exactly like an HTTP
197 This class handles Range headers exactly like an HTTP
202 server would.
198 server would.
203 """
199 """
204 def open_local_file(self, req):
200 def open_local_file(self, req):
205 import mimetypes
201 import mimetypes
206 import email
202 import email
207 host = req.get_host()
203 host = req.get_host()
208 file = req.get_selector()
204 file = req.get_selector()
209 localfile = urllib.url2pathname(file)
205 localfile = urllib.url2pathname(file)
210 stats = os.stat(localfile)
206 stats = os.stat(localfile)
211 size = stats[stat.ST_SIZE]
207 size = stats[stat.ST_SIZE]
212 modified = email.Utils.formatdate(stats[stat.ST_MTIME])
208 modified = email.Utils.formatdate(stats[stat.ST_MTIME])
213 mtype = mimetypes.guess_type(file)[0]
209 mtype = mimetypes.guess_type(file)[0]
214 if host:
210 if host:
215 host, port = urllib.splitport(host)
211 host, port = urllib.splitport(host)
216 if port or socket.gethostbyname(host) not in self.get_names():
212 if port or socket.gethostbyname(host) not in self.get_names():
217 raise urllib2.URLError('file not on local host')
213 raise urllib2.URLError('file not on local host')
218 fo = open(localfile,'rb')
214 fo = open(localfile,'rb')
219 brange = req.headers.get('Range', None)
215 brange = req.headers.get('Range', None)
220 brange = range_header_to_tuple(brange)
216 brange = range_header_to_tuple(brange)
221 assert brange != ()
217 assert brange != ()
222 if brange:
218 if brange:
223 (fb, lb) = brange
219 (fb, lb) = brange
224 if lb == '':
220 if lb == '':
225 lb = size
221 lb = size
226 if fb < 0 or fb > size or lb > size:
222 if fb < 0 or fb > size or lb > size:
227 raise RangeError('Requested Range Not Satisfiable')
223 raise RangeError('Requested Range Not Satisfiable')
228 size = (lb - fb)
224 size = (lb - fb)
229 fo = RangeableFileObject(fo, (fb, lb))
225 fo = RangeableFileObject(fo, (fb, lb))
230 headers = email.message_from_string(
226 headers = email.message_from_string(
231 'Content-Type: %s\nContent-Length: %d\nLast-Modified: %s\n' %
227 'Content-Type: %s\nContent-Length: %d\nLast-Modified: %s\n' %
232 (mtype or 'text/plain', size, modified))
228 (mtype or 'text/plain', size, modified))
233 return urllib.addinfourl(fo, headers, 'file:'+file)
229 return urllib.addinfourl(fo, headers, 'file:'+file)
234
230
235
231
236 # FTP Range Support
232 # FTP Range Support
237 # Unfortunately, a large amount of base FTP code had to be copied
233 # Unfortunately, a large amount of base FTP code had to be copied
238 # from urllib and urllib2 in order to insert the FTP REST command.
234 # from urllib and urllib2 in order to insert the FTP REST command.
239 # Code modifications for range support have been commented as
235 # Code modifications for range support have been commented as
240 # follows:
236 # follows:
241 # -- range support modifications start/end here
237 # -- range support modifications start/end here
242
238
243 from urllib import splitport, splituser, splitpasswd, splitattr, \
239 from urllib import splitport, splituser, splitpasswd, splitattr, \
244 unquote, addclosehook, addinfourl
240 unquote, addclosehook, addinfourl
245 import ftplib
241 import ftplib
246 import socket
242 import socket
247 import sys
243 import sys
248 import mimetypes
244 import mimetypes
249 import email
245 import email
250
246
251 class FTPRangeHandler(urllib2.FTPHandler):
247 class FTPRangeHandler(urllib2.FTPHandler):
252 def ftp_open(self, req):
248 def ftp_open(self, req):
253 host = req.get_host()
249 host = req.get_host()
254 if not host:
250 if not host:
255 raise IOError('ftp error', 'no host given')
251 raise IOError('ftp error', 'no host given')
256 host, port = splitport(host)
252 host, port = splitport(host)
257 if port is None:
253 if port is None:
258 port = ftplib.FTP_PORT
254 port = ftplib.FTP_PORT
259 else:
255 else:
260 port = int(port)
256 port = int(port)
261
257
262 # username/password handling
258 # username/password handling
263 user, host = splituser(host)
259 user, host = splituser(host)
264 if user:
260 if user:
265 user, passwd = splitpasswd(user)
261 user, passwd = splitpasswd(user)
266 else:
262 else:
267 passwd = None
263 passwd = None
268 host = unquote(host)
264 host = unquote(host)
269 user = unquote(user or '')
265 user = unquote(user or '')
270 passwd = unquote(passwd or '')
266 passwd = unquote(passwd or '')
271
267
272 try:
268 try:
273 host = socket.gethostbyname(host)
269 host = socket.gethostbyname(host)
274 except socket.error, msg:
270 except socket.error, msg:
275 raise urllib2.URLError(msg)
271 raise urllib2.URLError(msg)
276 path, attrs = splitattr(req.get_selector())
272 path, attrs = splitattr(req.get_selector())
277 dirs = path.split('/')
273 dirs = path.split('/')
278 dirs = map(unquote, dirs)
274 dirs = map(unquote, dirs)
279 dirs, file = dirs[:-1], dirs[-1]
275 dirs, file = dirs[:-1], dirs[-1]
280 if dirs and not dirs[0]:
276 if dirs and not dirs[0]:
281 dirs = dirs[1:]
277 dirs = dirs[1:]
282 try:
278 try:
283 fw = self.connect_ftp(user, passwd, host, port, dirs)
279 fw = self.connect_ftp(user, passwd, host, port, dirs)
284 type = file and 'I' or 'D'
280 type = file and 'I' or 'D'
285 for attr in attrs:
281 for attr in attrs:
286 attr, value = splitattr(attr)
282 attr, value = splitattr(attr)
287 if attr.lower() == 'type' and \
283 if attr.lower() == 'type' and \
288 value in ('a', 'A', 'i', 'I', 'd', 'D'):
284 value in ('a', 'A', 'i', 'I', 'd', 'D'):
289 type = value.upper()
285 type = value.upper()
290
286
291 # -- range support modifications start here
287 # -- range support modifications start here
292 rest = None
288 rest = None
293 range_tup = range_header_to_tuple(req.headers.get('Range', None))
289 range_tup = range_header_to_tuple(req.headers.get('Range', None))
294 assert range_tup != ()
290 assert range_tup != ()
295 if range_tup:
291 if range_tup:
296 (fb, lb) = range_tup
292 (fb, lb) = range_tup
297 if fb > 0:
293 if fb > 0:
298 rest = fb
294 rest = fb
299 # -- range support modifications end here
295 # -- range support modifications end here
300
296
301 fp, retrlen = fw.retrfile(file, type, rest)
297 fp, retrlen = fw.retrfile(file, type, rest)
302
298
303 # -- range support modifications start here
299 # -- range support modifications start here
304 if range_tup:
300 if range_tup:
305 (fb, lb) = range_tup
301 (fb, lb) = range_tup
306 if lb == '':
302 if lb == '':
307 if retrlen is None or retrlen == 0:
303 if retrlen is None or retrlen == 0:
308 raise RangeError('Requested Range Not Satisfiable due'
304 raise RangeError('Requested Range Not Satisfiable due'
309 ' to unobtainable file length.')
305 ' to unobtainable file length.')
310 lb = retrlen
306 lb = retrlen
311 retrlen = lb - fb
307 retrlen = lb - fb
312 if retrlen < 0:
308 if retrlen < 0:
313 # beginning of range is larger than file
309 # beginning of range is larger than file
314 raise RangeError('Requested Range Not Satisfiable')
310 raise RangeError('Requested Range Not Satisfiable')
315 else:
311 else:
316 retrlen = lb - fb
312 retrlen = lb - fb
317 fp = RangeableFileObject(fp, (0, retrlen))
313 fp = RangeableFileObject(fp, (0, retrlen))
318 # -- range support modifications end here
314 # -- range support modifications end here
319
315
320 headers = ""
316 headers = ""
321 mtype = mimetypes.guess_type(req.get_full_url())[0]
317 mtype = mimetypes.guess_type(req.get_full_url())[0]
322 if mtype:
318 if mtype:
323 headers += "Content-Type: %s\n" % mtype
319 headers += "Content-Type: %s\n" % mtype
324 if retrlen is not None and retrlen >= 0:
320 if retrlen is not None and retrlen >= 0:
325 headers += "Content-Length: %d\n" % retrlen
321 headers += "Content-Length: %d\n" % retrlen
326 headers = email.message_from_string(headers)
322 headers = email.message_from_string(headers)
327 return addinfourl(fp, headers, req.get_full_url())
323 return addinfourl(fp, headers, req.get_full_url())
328 except ftplib.all_errors, msg:
324 except ftplib.all_errors, msg:
329 raise IOError('ftp error', msg), sys.exc_info()[2]
325 raise IOError('ftp error', msg), sys.exc_info()[2]
330
326
331 def connect_ftp(self, user, passwd, host, port, dirs):
327 def connect_ftp(self, user, passwd, host, port, dirs):
332 fw = ftpwrapper(user, passwd, host, port, dirs)
328 fw = ftpwrapper(user, passwd, host, port, dirs)
333 return fw
329 return fw
334
330
335 class ftpwrapper(urllib.ftpwrapper):
331 class ftpwrapper(urllib.ftpwrapper):
336 # range support note:
332 # range support note:
337 # this ftpwrapper code is copied directly from
333 # this ftpwrapper code is copied directly from
338 # urllib. The only enhancement is to add the rest
334 # urllib. The only enhancement is to add the rest
339 # argument and pass it on to ftp.ntransfercmd
335 # argument and pass it on to ftp.ntransfercmd
340 def retrfile(self, file, type, rest=None):
336 def retrfile(self, file, type, rest=None):
341 self.endtransfer()
337 self.endtransfer()
342 if type in ('d', 'D'):
338 if type in ('d', 'D'):
343 cmd = 'TYPE A'
339 cmd = 'TYPE A'
344 isdir = 1
340 isdir = 1
345 else:
341 else:
346 cmd = 'TYPE ' + type
342 cmd = 'TYPE ' + type
347 isdir = 0
343 isdir = 0
348 try:
344 try:
349 self.ftp.voidcmd(cmd)
345 self.ftp.voidcmd(cmd)
350 except ftplib.all_errors:
346 except ftplib.all_errors:
351 self.init()
347 self.init()
352 self.ftp.voidcmd(cmd)
348 self.ftp.voidcmd(cmd)
353 conn = None
349 conn = None
354 if file and not isdir:
350 if file and not isdir:
355 # Use nlst to see if the file exists at all
351 # Use nlst to see if the file exists at all
356 try:
352 try:
357 self.ftp.nlst(file)
353 self.ftp.nlst(file)
358 except ftplib.error_perm, reason:
354 except ftplib.error_perm, reason:
359 raise IOError('ftp error', reason), sys.exc_info()[2]
355 raise IOError('ftp error', reason), sys.exc_info()[2]
360 # Restore the transfer mode!
356 # Restore the transfer mode!
361 self.ftp.voidcmd(cmd)
357 self.ftp.voidcmd(cmd)
362 # Try to retrieve as a file
358 # Try to retrieve as a file
363 try:
359 try:
364 cmd = 'RETR ' + file
360 cmd = 'RETR ' + file
365 conn = self.ftp.ntransfercmd(cmd, rest)
361 conn = self.ftp.ntransfercmd(cmd, rest)
366 except ftplib.error_perm, reason:
362 except ftplib.error_perm, reason:
367 if str(reason).startswith('501'):
363 if str(reason).startswith('501'):
368 # workaround for REST not supported error
364 # workaround for REST not supported error
369 fp, retrlen = self.retrfile(file, type)
365 fp, retrlen = self.retrfile(file, type)
370 fp = RangeableFileObject(fp, (rest,''))
366 fp = RangeableFileObject(fp, (rest,''))
371 return (fp, retrlen)
367 return (fp, retrlen)
372 elif not str(reason).startswith('550'):
368 elif not str(reason).startswith('550'):
373 raise IOError('ftp error', reason), sys.exc_info()[2]
369 raise IOError('ftp error', reason), sys.exc_info()[2]
374 if not conn:
370 if not conn:
375 # Set transfer mode to ASCII!
371 # Set transfer mode to ASCII!
376 self.ftp.voidcmd('TYPE A')
372 self.ftp.voidcmd('TYPE A')
377 # Try a directory listing
373 # Try a directory listing
378 if file:
374 if file:
379 cmd = 'LIST ' + file
375 cmd = 'LIST ' + file
380 else:
376 else:
381 cmd = 'LIST'
377 cmd = 'LIST'
382 conn = self.ftp.ntransfercmd(cmd)
378 conn = self.ftp.ntransfercmd(cmd)
383 self.busy = 1
379 self.busy = 1
384 # Pass back both a suitably decorated object and a retrieval length
380 # Pass back both a suitably decorated object and a retrieval length
385 return (addclosehook(conn[0].makefile('rb'),
381 return (addclosehook(conn[0].makefile('rb'),
386 self.endtransfer), conn[1])
382 self.endtransfer), conn[1])
387
383
388
384
389 ####################################################################
385 ####################################################################
390 # Range Tuple Functions
386 # Range Tuple Functions
391 # XXX: These range tuple functions might go better in a class.
387 # XXX: These range tuple functions might go better in a class.
392
388
393 _rangere = None
389 _rangere = None
394 def range_header_to_tuple(range_header):
390 def range_header_to_tuple(range_header):
395 """Get a (firstbyte,lastbyte) tuple from a Range header value.
391 """Get a (firstbyte,lastbyte) tuple from a Range header value.
396
392
397 Range headers have the form "bytes=<firstbyte>-<lastbyte>". This
393 Range headers have the form "bytes=<firstbyte>-<lastbyte>". This
398 function pulls the firstbyte and lastbyte values and returns
394 function pulls the firstbyte and lastbyte values and returns
399 a (firstbyte,lastbyte) tuple. If lastbyte is not specified in
395 a (firstbyte,lastbyte) tuple. If lastbyte is not specified in
400 the header value, it is returned as an empty string in the
396 the header value, it is returned as an empty string in the
401 tuple.
397 tuple.
402
398
403 Return None if range_header is None
399 Return None if range_header is None
404 Return () if range_header does not conform to the range spec
400 Return () if range_header does not conform to the range spec
405 pattern.
401 pattern.
406
402
407 """
403 """
408 global _rangere
404 global _rangere
409 if range_header is None:
405 if range_header is None:
410 return None
406 return None
411 if _rangere is None:
407 if _rangere is None:
412 import re
408 import re
413 _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
409 _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
414 match = _rangere.match(range_header)
410 match = _rangere.match(range_header)
415 if match:
411 if match:
416 tup = range_tuple_normalize(match.group(1, 2))
412 tup = range_tuple_normalize(match.group(1, 2))
417 if tup and tup[1]:
413 if tup and tup[1]:
418 tup = (tup[0], tup[1]+1)
414 tup = (tup[0], tup[1]+1)
419 return tup
415 return tup
420 return ()
416 return ()
421
417
422 def range_tuple_to_header(range_tup):
418 def range_tuple_to_header(range_tup):
423 """Convert a range tuple to a Range header value.
419 """Convert a range tuple to a Range header value.
424 Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
420 Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
425 if no range is needed.
421 if no range is needed.
426 """
422 """
427 if range_tup is None:
423 if range_tup is None:
428 return None
424 return None
429 range_tup = range_tuple_normalize(range_tup)
425 range_tup = range_tuple_normalize(range_tup)
430 if range_tup:
426 if range_tup:
431 if range_tup[1]:
427 if range_tup[1]:
432 range_tup = (range_tup[0], range_tup[1] - 1)
428 range_tup = (range_tup[0], range_tup[1] - 1)
433 return 'bytes=%s-%s' % range_tup
429 return 'bytes=%s-%s' % range_tup
434
430
435 def range_tuple_normalize(range_tup):
431 def range_tuple_normalize(range_tup):
436 """Normalize a (first_byte,last_byte) range tuple.
432 """Normalize a (first_byte,last_byte) range tuple.
437 Return a tuple whose first element is guaranteed to be an int
433 Return a tuple whose first element is guaranteed to be an int
438 and whose second element will be '' (meaning: the last byte) or
434 and whose second element will be '' (meaning: the last byte) or
439 an int. Finally, return None if the normalized tuple == (0,'')
435 an int. Finally, return None if the normalized tuple == (0,'')
440 as that is equivelant to retrieving the entire file.
436 as that is equivelant to retrieving the entire file.
441 """
437 """
442 if range_tup is None:
438 if range_tup is None:
443 return None
439 return None
444 # handle first byte
440 # handle first byte
445 fb = range_tup[0]
441 fb = range_tup[0]
446 if fb in (None, ''):
442 if fb in (None, ''):
447 fb = 0
443 fb = 0
448 else:
444 else:
449 fb = int(fb)
445 fb = int(fb)
450 # handle last byte
446 # handle last byte
451 try:
447 try:
452 lb = range_tup[1]
448 lb = range_tup[1]
453 except IndexError:
449 except IndexError:
454 lb = ''
450 lb = ''
455 else:
451 else:
456 if lb is None:
452 if lb is None:
457 lb = ''
453 lb = ''
458 elif lb != '':
454 elif lb != '':
459 lb = int(lb)
455 lb = int(lb)
460 # check if range is over the entire file
456 # check if range is over the entire file
461 if (fb, lb) == (0, ''):
457 if (fb, lb) == (0, ''):
462 return None
458 return None
463 # check that the range is valid
459 # check that the range is valid
464 if lb < fb:
460 if lb < fb:
465 raise RangeError('Invalid byte range: %s-%s' % (fb, lb))
461 raise RangeError('Invalid byte range: %s-%s' % (fb, lb))
466 return (fb, lb)
462 return (fb, lb)
General Comments 0
You need to be logged in to leave comments. Login now