vendor/tramline

changeset 33:5f66349d184c gracinet-fix-range

#2322: PUT requests
author Georges Racinet on purity.racinet.fr <georges@racinet.fr>
date Sun, 16 Jan 2011 09:48:58 +0100
parents ad965873c5a6
children d70f09d8a614
files src/tramline/core.py
diffstat 1 files changed, 94 insertions(+), 44 deletions(-) [+]
line diff
     1.1 --- a/src/tramline/core.py
     1.2 +++ b/src/tramline/core.py
     1.3 @@ -1,6 +1,6 @@
     1.4  import os, tempfile, random, sys, errno, mimetools
     1.5  import cgi
     1.6 -from mod_python import apache
     1.7 +from mod_python import apache, Cookie
     1.8  
     1.9  OPTION_ALLOW_GROUP_WRITE = 'allow_group_write'
    1.10  TRAMLINE_RANGE_HEADER = 'X-Tramline-Original-Range'
    1.11 @@ -87,7 +87,8 @@
    1.12          pass_on(filter)
    1.13          return
    1.14  
    1.15 -    if filter.req.method == 'GET':
    1.16 +    req_method = filter.req.method
    1.17 +    if req_method == 'GET':
    1.18          hin = filter.req.headers_in
    1.19          range = hin.get('Range')
    1.20          if range is not None:
    1.21 @@ -95,16 +96,27 @@
    1.22              # hoping that most backing apps issue 206, not 200 for that
    1.23              hin['Range'] = 'bytes=0-' 
    1.24  
    1.25 -    # we only handle POST requests
    1.26 -    if filter.req.method != 'POST':
    1.27 +    
    1.28 +    # we only affect POST and PUT requests from now on
    1.29 +    if filter.req.method not in ('POST', 'PUT'):
    1.30          pass_on(filter)
    1.31          return
    1.32  
    1.33 -    # we only handle multipart/form-data
    1.34 -    enctype = filter.req.headers_in.get('Content-Type')
    1.35 -    if enctype[:19] != 'multipart/form-data':
    1.36 -        pass_on(filter)
    1.37 -        return
    1.38 +    if req_method == 'POST':
    1.39 +        # we only handle multipart/form-data
    1.40 +        enctype = filter.req.headers_in.get('Content-Type')
    1.41 +        if enctype[:19] != 'multipart/form-data':
    1.42 +            pass_on(filter)
    1.43 +            return
    1.44 +    elif req_method == 'PUT':
    1.45 +        # we only handle request that have the 'X-Tramline-Enable' header
    1.46 +        # or 'tramline_enable' cookie. 
    1.47 +        # Cookie enabling is here to support those agents that
    1.48 +        # can't set a custom header (e.g., zopeedit)
    1.49 +        if filter.req.headers_in.get('X-Tramline-Enable') is None \
    1.50 +                and Cookie.get_cookie(filter.req, 'tramline_enable') is None:
    1.51 +            pass_on(filter)
    1.52 +            return
    1.53  
    1.54      # check whether we have an id already
    1.55      id = filter.req.headers_in.get('tramline_id')
    1.56 @@ -112,7 +124,7 @@
    1.57      if id is None:
    1.58          # no id, so create new processor instance and store
    1.59          # away id
    1.60 -        processor = theProcessorRegistry.createProcessor()
    1.61 +        processor = theProcessorRegistry.createProcessor(req_method=req_method)
    1.62          processor.initFromInputFilter(filter)
    1.63          filter.req.headers_in['tramline_id'] = str(processor.id)
    1.64      else:
    1.65 @@ -153,7 +165,7 @@
    1.66  
    1.67      # in case of post request, we may need to do a commit/abort
    1.68      # of previous input round
    1.69 -    if filter.req.method == 'POST':
    1.70 +    if filter.req.method in ('POST', 'PUT'):
    1.71          outputfilter_post(filter)
    1.72          return
    1.73      # in case of a get request, we may need to serve up files,
    1.74 @@ -324,20 +336,25 @@
    1.75  
    1.76      
    1.77  class ProcessorRegistry:
    1.78 +
    1.79 +    _processor_classes = {}
    1.80 +
    1.81      def __init__(self):
    1.82          self._processors = {}
    1.83  
    1.84      def getProcessor(self, id):
    1.85          return self._processors[id]
    1.86  
    1.87 -    def createProcessor(self):
    1.88 +    def createProcessor(self, req_method='POST'):
    1.89          # XXX thread issues?
    1.90          while True:
    1.91              id = random.randrange(sys.maxint)
    1.92              if id not in self._processors:
    1.93                  break
    1.94 -        result = self._processors[id] = Processor(id)
    1.95 -        return result
    1.96 +
    1.97 +        ProcessorClass = self._processor_classes.get(req_method)
    1.98 +        proc = self._processors[id] = ProcessorClass(id)
    1.99 +        return proc
   1.100      
   1.101      def removeProcessor(self, processor):
   1.102          del self._processors[processor.id]
   1.103 @@ -348,7 +365,7 @@
   1.104      return os.path.join(tramline_stat_path(req), progress_id)
   1.105  
   1.106  class Processor:
   1.107 -    """Processor for Post requests. 
   1.108 +    """Base class for processors.
   1.109  
   1.110      API atributes:
   1.111         id: this is the main identifier in the registry. It is used for 
   1.112 @@ -364,10 +381,9 @@
   1.113         uploaded: this is the total file data upload this processor has seen. 
   1.114             there might be more than one file in this request.
   1.115  
   1.116 -       upload_length: the full length of the POST request, as taken from the
   1.117 +       upload_length: the full length of the input request, as taken from the
   1.118         header
   1.119      """
   1.120 -       
   1.121      def __init__(self, id):
   1.122          self.uploaded = 0L
   1.123          self.id = id
   1.124 @@ -376,11 +392,7 @@
   1.125          self._upload_files = []
   1.126          self._incoming = []
   1.127  	self._isize = 0
   1.128 -        # we use a state pattern where the handle method gets
   1.129 -        # replaced by the current handle method for this state.
   1.130 -        self.handle = self.handle_first_boundary
   1.131 -        self.vars_to_handle = []
   1.132 -        self._enable_vars=''
   1.133 +        self._f = None # output file object
   1.134  
   1.135      def initFromInputFilter(self, filter):
   1.136          headers = filter.req.headers_in
   1.137 @@ -392,11 +404,6 @@
   1.138                # parse_qs always produces lists
   1.139                self.progress_id = filter.req.connection.remote_ip + '-' + gu_ids[0]
   1.140  
   1.141 -    def pushInput(self, data, out):
   1.142 -        lines = data.splitlines(True)
   1.143 -        for line in lines:
   1.144 -            self.pushInputLine(line, out)
   1.145 -
   1.146      def logProgress(self, req):
   1.147          """Log progress upload to a file for async requests to use
   1.148  
   1.149 @@ -417,12 +424,67 @@
   1.150              f.write(str({'state': 1, 'percent': int(percent)}))
   1.151              f.close()
   1.152  
   1.153 +    def commit(self, req):
   1.154 +        # XXX works under the assumption that the last segment of 
   1.155 +        # file path is the tramline id
   1.156 +        for upload_file in self._upload_files:
   1.157 +            dummy, filename = os.path.split(upload_file)
   1.158 +            os.rename(upload_file, id_to_path(tramline_path(req), filename))
   1.159 +
   1.160 +    def abort(self):
   1.161 +        for upload_file in self._upload_files:
   1.162 +            os.remove(upload_file)
   1.163 +
   1.164 +    def initUploadFile(self, out, with_newline=True):
   1.165 +        """Create the dump file, write id to out and keep needed references."""
   1.166 +        fd, pathname, file_id = createUniqueFile(out.req)
   1.167 +
   1.168 +        self._f = os.fdopen(fd, 'wb')
   1.169 +        self._upload_files.append(pathname)
   1.170 +        out.write(file_id)
   1.171 +        if with_newline:
   1.172 +            out.write('\r\n')
   1.173 +
   1.174 +class PutRequestProcessor(Processor):
   1.175 +   """Directly dump inconditionaly incoming data."""
   1.176 +
   1.177 +   def pushInput(self, data, out):
   1.178 +       log("PUT Pushing input %d" % len(data), out.req)
   1.179 +       if self._f is None:
   1.180 +           self.initUploadFile(out, with_newline=False)
   1.181 +       self._f.write(data)
   1.182 +
   1.183 +   def finalizeInput(self, out):
   1.184 +       out.req.headers_in['tramline'] = ''
   1.185 +       if self._f is not None: # one never knows
   1.186 +           self._f.close()
   1.187 +
   1.188 +class PostRequestProcessor(Processor):
   1.189 +    """Processor for Post requests. 
   1.190 +
   1.191 +    Handles multipart/form-data content, looks for enabling info in the form
   1.192 +    data, and if enabled, intercept those parts that are file uploads.
   1.193 +    """
   1.194 +
   1.195 +    def __init__(self, pid):
   1.196 +        Processor.__init__(self, pid)
   1.197 +        # we use a state pattern where the handle method gets
   1.198 +        # replaced by the current handle method for this state.
   1.199 +        self.handle = self.handle_first_boundary
   1.200 +        self.vars_to_handle = []
   1.201 +        self._enable_vars=''
   1.202 +
   1.203 +    def pushInput(self, data, out):
   1.204 +        lines = data.splitlines(True)
   1.205 +        for line in lines:
   1.206 +            self.pushInputLine(line, out)
   1.207 +
   1.208      def pushInputLine(self, data, out):
   1.209          # collect data
   1.210          self._incoming.append(data)
   1.211  	self._isize += len(data)
   1.212  
   1.213 -        # if we're not at the end of the line, input was broken
   1.214 +       # if we're not at the end of the line, input was broken
   1.215          # somewhere, unless we are handling file data which might be binary.
   1.216  	# We return to collect more first (also for file data if too small)
   1.217          if data[-1] != '\n' and (
   1.218 @@ -444,17 +506,6 @@
   1.219          if self._upload_files:
   1.220              out.req.headers_in['tramline'] = ''
   1.221  
   1.222 -    def commit(self, req):
   1.223 -        # XXX works under the assumption that the last segment of 
   1.224 -        # file path is the tramline id
   1.225 -        for upload_file in self._upload_files:
   1.226 -            dummy, filename = os.path.split(upload_file)
   1.227 -            os.rename(upload_file, id_to_path(tramline_path(req), filename))
   1.228 -
   1.229 -    def abort(self):
   1.230 -        for upload_file in self._upload_files:
   1.231 -            os.remove(upload_file)
   1.232 -    
   1.233      def handle_first_boundary(self, line, out):
   1.234          self._boundary = line
   1.235          self._last_boundary = self._boundary.rstrip() + '--\r\n'
   1.236 @@ -500,12 +551,8 @@
   1.237                self._disposition_options.get('name') not in self.vars_to_handle:
   1.238              self.handle = self.handle_data
   1.239              return
   1.240 -        fd, pathname, file_id = createUniqueFile(out.req)
   1.241  
   1.242 -        self._f = os.fdopen(fd, 'wb')
   1.243 -        self._upload_files.append(pathname)
   1.244 -        out.write(file_id)
   1.245 -        out.write('\r\n')
   1.246 +        self.initUploadFile(out)
   1.247          
   1.248          self._previous_line = None
   1.249          self.handle = self.handle_file_data
   1.250 @@ -626,6 +673,9 @@
   1.251                  continue # try again
   1.252              raise
   1.253  
   1.254 +ProcessorRegistry._processor_classes = dict(PUT=PutRequestProcessor,
   1.255 +                                            POST=PostRequestProcessor)
   1.256 +
   1.257  def log(data, req):
   1.258      f = open(os.path.join(tramline_path(req), 'tramline.log'), 'ab')
   1.259      f.write(data)