Skip to content

Commit

Permalink
add possibility to return a file. Use sendfile API if it's available
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitc committed May 3, 2011
1 parent d1e45cf commit 9d8c0b4
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 1 deletion.
7 changes: 6 additions & 1 deletion NOTICE
Expand Up @@ -16,7 +16,12 @@ gunicorn/config.py
gunicorn/pidfile.py
gunicorn/workertmp.py


backports from python 2.7/3.x
-----------------------------
tools.import_module


gunicorn/_sendfile.py
---------------------

based on gunicorn/sendfile.py refactored
35 changes: 35 additions & 0 deletions README.rst
Expand Up @@ -145,6 +145,22 @@ Example SOCKS4 Proxy in 18 Lines
else:
return {"close": "\0\x5b\0\0\0\0\0\0"}

Example of returning a file
---------------------------

::

import os

WELCOME_FILE = os.path.join(os.path.dirname(__file__), "welcome.txt")

def proxy(data):
fno = os.open(WELCOME_FILE, os.O_RDONLY)
return {
"file": fno,
"reply": "HTTP/1.1 200 OK\r\n\r\n"
}

Valid return values
-------------------

Expand All @@ -158,6 +174,25 @@ Valid return values
* { "close": True } - Close the connection.
* { "close": String } - Close the connection after sending
the String.
* { "file": String } - Return a file specify by the file path and close
the connection.
* { "file": String, "reply": String } - Return a file specify by the
file path and close the connection.
* { "file": Int, "reply": String} - Same as above but reply with given
data back to the client
* { "file": Int } - Return a file specify by
its file descriptor * { "file": Int, "reply": String} - Same as above
but reply with given data back to the client


**"file"** command can have 2 optionnnal parameters:

- offset: argument specifies where to begin in the file.
- nbytes: specifies how many bytes of the file should be sent

If `sendfile <http://en.wikipedia.org/wiki/Sendfile>`_ API available it
will be used to send a file with "file" command.


To handle ssl for remote connection you can add these optionals
arguments:
Expand Down
10 changes: 10 additions & 0 deletions examples/sendfile.py
@@ -0,0 +1,10 @@
import os

WELCOME_FILE = os.path.join(os.path.dirname(__file__), "welcome.txt")

def proxy(data):
fno = os.open(WELCOME_FILE, os.O_RDONLY)
return {
"file": fno,
"reply": "HTTP/1.1 200 OK\r\n\r\n"
}
1 change: 1 addition & 0 deletions examples/welcome.txt
@@ -0,0 +1 @@
hello!
66 changes: 66 additions & 0 deletions tproxy/_sendfile.py
@@ -0,0 +1,66 @@
# -*- coding: utf-8 -
#
# This file is part of tproxy released under the MIT license.
# See the NOTICE for more information.

import errno
import os
import sys

try:
import ctypes
import ctypes.util
except MemoryError:
# selinux execmem denial
# https://bugzilla.redhat.com/show_bug.cgi?id=488396
raise ImportError

SUPPORTED_PLATFORMS = (
'darwin',
'freebsd',
'dragonfly'
'linux2')

if sys.version_info < (2, 6) or \
sys.platform not in SUPPORTED_PLATFORMS:
raise ImportError("sendfile isn't supported on this platform")

_libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
_sendfile = _libc.sendfile

def sendfile(fdout, fdin, offset, nbytes):
if sys.platform == 'darwin':
_sendfile.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_uint64,
ctypes.POINTER(ctypes.c_uint64), ctypes.c_voidp,
ctypes.c_int]
_nbytes = ctypes.c_uint64(nbytes)
result = _sendfile(fdin, fdout, offset, _nbytes, None, 0)
if result == -1:
e = ctypes.get_errno()
if e == errno.EAGAIN and _nbytes.value:
return nbytes.value
raise OSError(e, os.strerror(e))
return _nbytes.value
elif sys.platform in ('freebsd', 'dragonfly',):
_sendfile.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_uint64,
ctypes.c_uint64, ctypes.c_voidp,
ctypes.POINTER(ctypes.c_uint64), ctypes.c_int]
_sbytes = ctypes.c_uint64(nbytes)
result = _sendfile(fdin, fdout, offset, nbytes, None, _sbytes, 0)
if result == -1:
e = ctypes.get_errno()
if e == errno.EAGAIN and _sbytes.value:
return _sbytes.value
raise OSError(e, os.strerror(e))
return _sbytes.value

else:
_sendfile.argtypes = [ctypes.c_int, ctypes.c_int,
ctypes.POINTER(ctypes.c_uint64), ctypes.c_size_t]

_offset = ctypes.c_uint64(offset)
sent = _sendfile(fdout, fdin, _offset, nbytes)
if sent == -1:
e = ctypess.get_errno()

This comment has been minimized.

Copy link
@nkchenz

nkchenz Mar 4, 2012

typo in here? ctypes?

raise OSError(e, os.strerror(e))
return sent
20 changes: 20 additions & 0 deletions tproxy/client.py
Expand Up @@ -4,6 +4,7 @@
# See the NOTICE for more information.

import logging
import os
import ssl

import gevent
Expand All @@ -13,6 +14,7 @@

from .server import ServerConnection, InactivityTimeout
from .util import parse_address, is_ipv6
from .sendfile import async_sendfile

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -105,6 +107,24 @@ def do_proxy(self):
if isinstance(commands['close'], basestring):
self.send_data(self.sock, commands['close'])
raise StopIteration()

elif 'file' in commands:
# command to send a file
if isinstance(commands['file'], basestring):
fdin = os.open(commands['file'], os.O_RDONLY)
else:
fdin = commands['file']

offset = commands.get('offset', 0)
nbytes = commands.get('nbytes', os.fstat(fdin).st_size)

# send a reply if needed, useful in HTTP response.
if 'reply' in commands:
self.send_data(self.sock, commands['reply'])

# use sendfile if possible to send the file content
async_sendfile(self.sock.fileno(), fdin, offset, nbytes)
raise StopIteration()
else:
raise StopIteration()

Expand Down
46 changes: 46 additions & 0 deletions tproxy/sendfile.py
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -
#
# This file is part of tproxy released under the MIT license.
# See the NOTICE for more information.

import errno
import io
import os
try:
from os import sendfile
except ImportError:
try:
from _sendfile import sendfile
except ImportError:
def sendfile(fdout, fdin, offset, nbytes):
fsize = os.fstat(fdin).st_size

# max to send
length = min(fsize-offset, nbytes)

with os.fdopen(fdin) as fin:
fin.seek(offset)

while length > 0:
l = min(length, io.DEFAULT_BUFFER_SIZE)
os.write(fdout, fin.read(l))
length = length - l

return length

from gevent.socket import wait_write


def async_sendfile(fdout, fdin, offset, nbytes):
total_sent = 0
while total_sent < nbytes:
try:
sent = sendfile(fdout, fdin, offset + total_sent,
nbytes - total_sent)
total_sent += sent
except OSError, e:
if e.args[0] == errno.EAGAIN:
wait_write(fdout)
else:
raise
return total_sent

0 comments on commit 9d8c0b4

Please sign in to comment.