Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

Ticket #859 (enhancement)

Opened 2 months ago

Last modified 2 months ago

For requests with a Ranges header, static.serve_file() reads the entire range of file into memory

Status: closed (fixed)

Reported by: visteya Assigned to: visteya
Priority: normal Milestone: 3.1
Component: CherryPy code Keywords: range file_generator
Cc:

If static.serve_file() handles a request which has a Ranges header, it will read the entire range of the file into memory. For very large files, this is undesirable behavior.

The following patch uses file_generator() to read the file in reasonable-sized chunks. The patch is against svn trunk.

Also, this patch revealed a debatable design flaw in the current file_generator() function: file_generator() takes an open file object as an argument, but closes the file object before it returns. It would be better if file_generator() left the responsibility for closing the file object to the entity who opened it.

In the interest of not breaking existing code I am unaware of, in this patch I preserved the file-closing behavior of file_generator() if EOF is reached. But I propose removing the input.close() statement from file_generator(). I request comments on this proposal.

diff -r a131852b482a cherrypy/lib/__init__.py
--- a/cherrypy/lib/__init__.py  Thu Sep 18 11:41:21 2008 -0500
+++ b/cherrypy/lib/__init__.py  Thu Sep 18 20:18:50 2008 -0500
@@ -134,13 +134,32 @@ def unrepr(s):
     return _Builder().build(obj)


-def file_generator(input, chunkSize=65536):
-    """Yield the given input (a file object) in chunks (default 64k). (Core)"""
+def file_generator(input, length=None, chunkSize=65536):
+    """Yield the given input (a file object) in chunks (default 64k). (Core)
+    If length is specified, it must be a positive integer, and
+    the generator will stop after emitting that many bytes.
+    """
+    if length is not None:
+        if not isinstance(length, (int, long)):
+            raise TypeError("length must be an integer or long")
+        if length <= 0:
+            raise ValueError("length must be a positive integer or long")
+
+    count = 0
     chunk = input.read(chunkSize)
     while chunk:
-        yield chunk
+        if length is None:
+            yield chunk
+        else:
+            if count + len(chunk) <= length:
+                yield chunk
+                count += len(chunk)
+            else:
+                if length != count:
+                    yield chunk[:length-count]
+                break
         chunk = input.read(chunkSize)
-    input.close()

+    if not chunk:
+        input.close()

-
diff -r a131852b482a cherrypy/lib/static.py
--- a/cherrypy/lib/static.py    Thu Sep 18 11:41:21 2008 -0500
+++ b/cherrypy/lib/static.py    Thu Sep 18 18:59:04 2008 -0500
@@ -10,7 +10,7 @@ import urllib
 import urllib

 import cherrypy
-from cherrypy.lib import cptools, http
+from cherrypy.lib import cptools, http, file_generator


 def serve_file(path, content_type=None, disposition=None, name=None):
@@ -83,13 +83,15 @@ def serve_file(path, content_type=None,
             if len(r) == 1:
                 # Return a single-part response.
                 start, stop = r[0]
+                if stop > c_len:
+                    stop = c_len
                 r_len = stop - start
                 response.status = "206 Partial Content"
                 response.headers['Content-Range'] = ("bytes %s-%s/%s" %
                                                        (start, stop - 1, c_len))
                 response.headers['Content-Length'] = r_len
                 bodyfile.seek(start)
-                response.body = bodyfile.read(r_len)
+                response.body = file_generator(bodyfile, length=r_len)
             else:
                 # Return a multipart/byteranges response.
                 response.status = "206 Partial Content"
@@ -111,7 +113,8 @@ def serve_file(path, content_type=None,
                         yield ("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
                                % (start, stop - 1, c_len))
                         bodyfile.seek(start)
-                        yield bodyfile.read(stop - start)
+                        for chunk in file_generator(bodyfile, length=stop-start):
+                            yield chunk
                         yield "\r\n"
                     # Final boundary
                     yield "--" + boundary + "--"

Change History

09/18/08 22:24:45: Modified by visteya

Note this also resolves Ticket #849. I should have noticed that earlier, and appended to that ticket instead of opening a new one.

09/27/08 17:48:28: Modified by fumanchu

Looks good to me; trunk it once you fix all callers to call close() on their own.

09/28/08 02:27:41: Modified by visteya

  • status changed from new to closed.
  • resolution set to fixed.

Closed with [2047].

Hosted by WebFaction

Log in as guest/cpguest to create tickets