1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | |
---|
4 | """ |
---|
5 | | This file is part of the web2py Web Framework |
---|
6 | | Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> |
---|
7 | | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) |
---|
8 | |
---|
9 | Facilities to handle file streaming |
---|
10 | ------------------------------------ |
---|
11 | """ |
---|
12 | |
---|
13 | import os |
---|
14 | import stat |
---|
15 | import time |
---|
16 | import re |
---|
17 | import errno |
---|
18 | from gluon.http import HTTP |
---|
19 | from gluon.utils import unlocalised_http_header_date |
---|
20 | from gluon.contenttype import contenttype |
---|
21 | from gluon._compat import PY2 |
---|
22 | |
---|
23 | |
---|
24 | regex_start_range = re.compile('\d+(?=\-)') |
---|
25 | regex_stop_range = re.compile('(?<=\-)\d+') |
---|
26 | |
---|
27 | DEFAULT_CHUNK_SIZE = 64 * 1024 |
---|
28 | |
---|
29 | def streamer(stream, chunk_size=DEFAULT_CHUNK_SIZE, bytes=None, callback=None): |
---|
30 | try: |
---|
31 | offset = 0 |
---|
32 | while bytes is None or offset < bytes: |
---|
33 | if not bytes is None and bytes - offset < chunk_size: |
---|
34 | chunk_size = bytes - offset |
---|
35 | data = stream.read(chunk_size) |
---|
36 | length = len(data) |
---|
37 | if not length: |
---|
38 | break |
---|
39 | else: |
---|
40 | yield data |
---|
41 | if length < chunk_size: |
---|
42 | break |
---|
43 | offset += length |
---|
44 | finally: |
---|
45 | stream.close() |
---|
46 | if callback: |
---|
47 | callback() |
---|
48 | |
---|
49 | def stream_file_or_304_or_206( |
---|
50 | static_file, |
---|
51 | chunk_size=DEFAULT_CHUNK_SIZE, |
---|
52 | request=None, |
---|
53 | headers={}, |
---|
54 | status=200, |
---|
55 | error_message=None |
---|
56 | ): |
---|
57 | # FIX THIS |
---|
58 | # if error_message is None: |
---|
59 | # error_message = rewrite.THREAD_LOCAL.routes.error_message % 'invalid request' |
---|
60 | try: |
---|
61 | if PY2: |
---|
62 | open_f = file # this makes no sense but without it GAE cannot open files |
---|
63 | else: |
---|
64 | open_f = open |
---|
65 | fp = open_f(static_file,'rb') |
---|
66 | except IOError as e: |
---|
67 | if e.errno == errno.EISDIR: |
---|
68 | raise HTTP(403, error_message, web2py_error='file is a directory') |
---|
69 | elif e.errno == errno.EACCES: |
---|
70 | raise HTTP(403, error_message, web2py_error='inaccessible file') |
---|
71 | else: |
---|
72 | raise HTTP(404, error_message, web2py_error='invalid file') |
---|
73 | else: |
---|
74 | fp.close() |
---|
75 | stat_file = os.stat(static_file) |
---|
76 | fsize = stat_file[stat.ST_SIZE] |
---|
77 | modified = stat_file[stat.ST_MTIME] |
---|
78 | mtime = unlocalised_http_header_date(time.gmtime(modified)) |
---|
79 | headers.setdefault('Content-Type', contenttype(static_file)) |
---|
80 | headers.setdefault('Last-Modified', mtime) |
---|
81 | headers.setdefault('Pragma', 'cache') |
---|
82 | headers.setdefault('Cache-Control', 'private') |
---|
83 | |
---|
84 | # if this is a normal response and not a respnse to an error page |
---|
85 | if status == 200: |
---|
86 | if request and request.env.http_if_modified_since == mtime: |
---|
87 | raise HTTP(304, **{'Content-Type': headers['Content-Type']}) |
---|
88 | |
---|
89 | elif request and request.env.http_range: |
---|
90 | start_items = regex_start_range.findall(request.env.http_range) |
---|
91 | if not start_items: |
---|
92 | start_items = [0] |
---|
93 | stop_items = regex_stop_range.findall(request.env.http_range) |
---|
94 | if not stop_items or int(stop_items[0]) > fsize - 1: |
---|
95 | stop_items = [fsize - 1] |
---|
96 | part = (int(start_items[0]), int(stop_items[0]), fsize) |
---|
97 | bytes = part[1] - part[0] + 1 |
---|
98 | try: |
---|
99 | stream = open(static_file, 'rb') |
---|
100 | except IOError as e: |
---|
101 | if e.errno in (errno.EISDIR, errno.EACCES): |
---|
102 | raise HTTP(403) |
---|
103 | else: |
---|
104 | raise HTTP(404) |
---|
105 | stream.seek(part[0]) |
---|
106 | headers['Content-Range'] = 'bytes %i-%i/%i' % part |
---|
107 | headers['Content-Length'] = '%i' % bytes |
---|
108 | status = 206 |
---|
109 | # in all the other cases (not 304, not 206, but 200 or error page) |
---|
110 | if status != 206: |
---|
111 | enc = request.env.http_accept_encoding |
---|
112 | if enc and 'gzip' in enc and not 'Content-Encoding' in headers: |
---|
113 | gzipped = static_file + '.gz' |
---|
114 | if os.path.isfile(gzipped) and os.path.getmtime(gzipped) >= modified: |
---|
115 | static_file = gzipped |
---|
116 | fsize = os.path.getsize(gzipped) |
---|
117 | headers['Content-Encoding'] = 'gzip' |
---|
118 | headers['Vary'] = 'Accept-Encoding' |
---|
119 | try: |
---|
120 | stream = open(static_file, 'rb') |
---|
121 | except IOError as e: |
---|
122 | # this better not happen when returning an error page ;-) |
---|
123 | if e.errno in (errno.EISDIR, errno.EACCES): |
---|
124 | raise HTTP(403) |
---|
125 | else: |
---|
126 | raise HTTP(404) |
---|
127 | headers['Content-Length'] = fsize |
---|
128 | bytes = None |
---|
129 | if request and request.env.web2py_use_wsgi_file_wrapper: |
---|
130 | wrapped = request.env.wsgi_file_wrapper(stream, chunk_size) |
---|
131 | else: |
---|
132 | wrapped = streamer(stream, chunk_size=chunk_size, bytes=bytes) |
---|
133 | raise HTTP(status, wrapped, **headers) |
---|