Permalink
Browse files

Added file upload functionality

  • Loading branch information...
1 parent 0a34823 commit 219fd2a80fd8c403b2553b189e05bf3a0b38a08c @rhymes committed Aug 14, 2010
Showing with 128 additions and 9 deletions.
  1. +2 −1 pinder/campfire.py
  2. +14 −6 pinder/connector.py
  3. +104 −0 pinder/multipart.py
  4. +8 −2 pinder/room.py
View
@@ -39,7 +39,8 @@ def find_room_by_name(self, name):
rooms = self.rooms()
for room in rooms:
if room['name'] == name:
- return Room(self, room['id'], data=room, connector=self._connector)
+ return Room(self, room['id'],
+ data=room, connector=self._connector)
def users(self, *rooms_ids):
"Returns info about users in any room or in the given room(s)."
View
@@ -6,6 +6,7 @@
import simplejson as json
from pinder.exc import HTTPUnauthorizedException, HTTPNotFoundException
+from pinder.multipart import encode_multipart, BOUNDARY
class HTTPConnector(object):
@@ -24,22 +25,29 @@ def __init__(self, subdomain, token, ssl=False, ua=''):
def get(self, path='', data=None, headers=None):
return self._request('GET', path, data, headers)
- def post(self, path, data=None, headers=None):
- return self._request('POST', path, data, headers)
+ def post(self, path, data=None, headers=None, file_upload=False):
+ return self._request('POST', path, data, headers, file_upload)
def put(self, path, data=None, headers=None):
return self._request('PUT', path, data, headers)
def _uri_for(self, path=''):
return "%s/%s.json" % (urlparse.urlunparse(self.uri), path)
- def _request(self, method, path, data=None, additional_headers=None):
+ def _request(self, method, path, data=None, additional_headers=None, file_upload=False):
additional_headers = additional_headers or dict()
- data = json.dumps(data or dict())
-
+ data = data or dict()
+
headers = {}
headers['user-agent'] = self.user_agent
- headers['content-type'] = 'application/json'
+
+ if method.upper() == 'POST' and file_upload:
+ data = encode_multipart(BOUNDARY, data)
+ headers['content-type'] = 'multipart/form-data; boundary=%s' % BOUNDARY
+ else:
+ data = json.dumps(data)
+ headers['content-type'] = 'application/json'
+
headers['content-length'] = str(len(data))
headers.update(additional_headers)
View
@@ -0,0 +1,104 @@
+# BSD LICENSE
+# adapted from:
+# http://code.djangoproject.com/browser/django/tags/releases/1.2.1/django/test/client.py
+# http://code.djangoproject.com/browser/django/tags/releases/1.2.1/django/utils/encoding.py
+# http://code.djangoproject.com/browser/django/tags/releases/1.2.1/django/utils/itercompat.py
+import mimetypes
+import os
+import types
+
+BOUNDARY = 'BoUnDaRyStRiNg'
+
+def is_iterable(x):
+ "A implementation independent way of checking for iterables"
+ try:
+ iter(x)
+ except TypeError:
+ return False
+ else:
+ return True
+
+def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
+ """
+ Returns a bytestring version of 's', encoded as specified in 'encoding'.
+
+ If strings_only is True, don't convert (some) non-string-like objects.
+ """
+ if strings_only and isinstance(s, (types.NoneType, int)):
+ return s
+
+ if not isinstance(s, basestring):
+ try:
+ return str(s)
+ except UnicodeEncodeError:
+ if isinstance(s, Exception):
+ # An Exception subclass containing non-ASCII data that doesn't
+ # know how to print itself properly. We shouldn't raise a
+ # further exception.
+ return ' '.join([smart_str(arg, encoding, strings_only,
+ errors) for arg in s])
+ return unicode(s).encode(encoding, errors)
+ elif isinstance(s, unicode):
+ return s.encode(encoding, errors)
+ elif s and encoding != 'utf-8':
+ return s.decode('utf-8', errors).encode(encoding, errors)
+ else:
+ return s
+
+def encode_multipart(boundary, data):
+ """
+ Encodes multipart POST data from a dictionary of form values.
+
+ The key will be used as the form data name; the value will be transmitted
+ as content. If the value is a file, the contents of the file will be guessed or sent
+ as an application/octet-stream; otherwise, str(value) will be sent.
+ """
+ lines = []
+
+ # Not by any means perfect, but good enough for our purposes.
+ is_file = lambda thing: hasattr(thing, "read") and callable(thing.read)
+
+ # Each bit of the multipart form data could be either a form value or a
+ # file, or a *list* of form values and/or files. Remember that HTTP field
+ # names can be duplicated!
+ for (key, value) in data.items():
+ if is_file(value):
+ lines.extend(encode_file(boundary, key, value))
+ elif not isinstance(value, basestring) and is_iterable(value):
+ for item in value:
+ if is_file(item):
+ lines.extend(encode_file(boundary, key, item))
+ else:
+ lines.extend([
+ '--' + boundary,
+ 'Content-Disposition: form-data; name="%s"' % smart_str(key),
+ '',
+ smart_str(item)
+ ])
+ else:
+ lines.extend([
+ '--' + boundary,
+ 'Content-Disposition: form-data; name="%s"' % smart_str(key),
+ '',
+ smart_str(value)
+ ])
+
+ lines.extend([
+ '--' + boundary + '--',
+ '',
+ ])
+ return '\r\n'.join(lines)
+
+def guess_mime(filepath):
+ mime, _ = mimetypes.guess_type(filepath)
+ return mime or 'application/octet-stream'
+
+def encode_file(boundary, key, file_):
+ return [
+ '--' + boundary,
+ 'Content-Disposition: form-data; name="%s"; filename="%s"' \
+ % (smart_str(key), smart_str(os.path.basename(file_.name))),
+ 'Content-Type: %s' % guess_mime(file_.name),
+ '',
+ file.read()
+ ]
View
@@ -30,8 +30,9 @@ def _path_for_room(self, path):
def _get(self, path='', data=None, headers=None):
return self._connector.get(self._path_for_room(path), data, headers)
- def _post(self, path, data=None, headers=None):
- return self._connector.post(self._path_for_room(path), data, headers)
+ def _post(self, path, data=None, headers=None, file_upload=False):
+ return self._connector.post(
+ self._path_for_room(path), data, headers, file_upload)
def _put(self, path, data=None, headers=None):
return self._connector.put(self._path_for_room(path), data, headers)
@@ -88,3 +89,8 @@ def update(self, name, topic):
"Updates name and/or topic of the room."
data = {'room': {'name': name, 'topic': topic}}
self._put('', data)
+
+ def upload(self, fileobj):
+ "Uploads the content of the given file-like object to the room."
+ data = {'upload': fileobj}
+ self._post('uploads', data, file_upload=True)

0 comments on commit 219fd2a

Please sign in to comment.