Skip to content

Commit

Permalink
Really close -- multipart-form parsing basically works, but sends fil…
Browse files Browse the repository at this point in the history
…es with a spare \r\n
  • Loading branch information
A. Jesse Jiryu Davis authored and A. Jesse Jiryu Davis committed Nov 4, 2011
1 parent 71c1af7 commit 8efe4f3
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 166 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
static/syncsend.js
.DS_Store
.idea
29 changes: 17 additions & 12 deletions syncsend.coffee
Expand Up @@ -4,6 +4,9 @@
# https://github.com/ajdavis/SyncSend
#

config =
use_ajax_upload: no

# options: callback, cancel (another callback), email (the email address)
make_uploader = (options) ->
new qq.FileUploader # qq.FileUploader from http://valums.com/ajax-upload/
Expand Down Expand Up @@ -38,26 +41,28 @@ class SyncSend
submit_send_email_form: (e) =>
$send_email_form = $ e.target
$send_email_form.attr 'disabled', true
email = $send_email_form.find('input[name="email"]').val()

$send_file_form = $('#send_file')
$file_input = $send_file_form.find('input[type="file"]')
$submit_button = $send_file_form.find('input[type="submit"]')

uploader = make_uploader
email: email
callback: (id, fileName, responseJSON) ->
# Nada
cancel: (id, fileName) ->
# Null
email = $send_email_form.find('input[name="email"]').val()
$send_file_form.attr 'action', '/api/' + encodeURIComponent email

if config.use_ajax_upload
uploader = make_uploader
email: email
callback: (id, fileName, responseJSON) ->
# Nada
cancel: (id, fileName) ->
# Null

# Won't be needing the regular upload field any more
$file_input.hide()
$submit_button.hide()

$send_file_form.find('input[name="email"]').val email
$send_file_form.fadeIn()

# Won't be needing the regular upload field any more
$file_input.hide()
$submit_button.hide()

return false
submit_receive_form: (e) =>
$form = $ '#receive'
Expand Down
36 changes: 23 additions & 13 deletions syncsend.py
Expand Up @@ -8,6 +8,7 @@
# code should be here, rather than in upload.py
#
import argparse
import cgi

import json

Expand All @@ -24,50 +25,57 @@
get_requests = {}

class SyncSendUploadRequest(FileUploadRequest):
def __init__(self, channel, path, queued):
FileUploadRequest.__init__(self, channel, path, queued)
def __init__(self, channel, path):
FileUploadRequest.__init__(self, channel, path)
if 'email' in self.args:
self.file_upload_path = self.path.rstrip('/') + '/' + self.args['email'][0]
else:
self.file_upload_path = self.path

print 'POST', self.file_upload_path
post_requests[self.file_upload_path] = self

def gotLength(self, length):
"""
Set self.is_form to True or False
"""
# TODO: terser
ctypes_raw = self.requestHeaders.getRawHeaders('content-type')
content_type, type_options = cgi.parse_header(ctypes_raw[0] if ctypes_raw else '')
self.is_form = content_type.lower() == 'multipart/form-data'

def fileStarted(self, filename, content_type):
print 'started', filename
self.filename = filename
self.content_type = content_type
self.sent_headers = False
if self.file_upload_path not in get_requests:
# Wait for receiver to connect
self.pause()
print 'pausing upload'
self.channel.pauseProducing()

def handleFileChunk(self, data):
def handleFileChunk(self, filename, data):
get_request = get_requests[self.file_upload_path]
if not self.sent_headers:
# Note that we don't set content-length - we can't know it.
# TODO: unittest weird filenames, determine what the escaping standard is for filenames
get_request.setHeader(
'Content-Disposition',
'attachment; filename="%s"' % self.filename.replace('"', ''),
)
get_request.setHeader(
'Content-Type',
self.content_type,
)
get_request.setHeader(
'Content-Length',
self.content_length,
self.channel.content_type, # TODO: decouple
)
self.sent_headers = True

get_request.write(data)

def fileCompleted(self):
# TODO: multiple files
print 'finished'
print 'finished file upload'

def process(self):
print 'POST process()'
self.setResponseCode(200)

# For fileuploader.js, which expects a JSON status
Expand All @@ -84,14 +92,16 @@ def requestReceived(self, command, path, version):
"""
Receiver has started downloading file
"""
print 'GET', path
print 'GET requestReceived()'
FileDownloadRequest.requestReceived(self, command, path, version)
get_requests[self.file_download_path] = self
if self.file_download_path in post_requests:
post_requests[self.file_download_path].resume()
print 'resuming upload'
post_requests[self.file_download_path].channel.resumeProducing()
return twisted.web.server.NOT_DONE_YET

def finish(self):
print 'GET finish()'
del get_requests[self.file_download_path]
FileDownloadRequest.finish(self)

Expand Down
34 changes: 28 additions & 6 deletions test/test_syncsend2.py
Expand Up @@ -23,7 +23,8 @@ def _test_transfer(self, postdata, key, send_first, content_type, expected_value
@param content_type: POST request's content-type
@param expected_value: If not None, the expected data to download (otherwise we expect postdata)
"""
print 'unittester pid:', os.getpid()
print 'postdata:\n%s' % repr(postdata)
print 'length', len(postdata)

start_server = os.environ.get('SYNCSEND_TEST_NO_SERVER', '').upper() != 'TRUE'

Expand Down Expand Up @@ -85,20 +86,41 @@ def get_fn(shared):

# post and get processes should be gone by now; make sure (we'll assert later
# that they terminated on their own, to catch problems in the test harness)
for p in post, get:
for name in 'post', 'get':
p = locals()[name]
if p.is_alive():
print 'killing', name, 'request'
kill(p)

self.assert_('get_result' in shared, "GET request did not complete")

def clip_data(data):
"""
Clip long data, displaying only the beginning and end
"""
length = 40
r = repr(data)
if len(r) <= length:
return r

return '%s...%s' % (r[:length/2], r[-length/2:])

ev = postdata if expected_value is None else expected_value
clipped_ev = clip_data(ev)
clipped_result = clip_data(shared['get_result'])
self.assertEqual(
postdata if expected_value is None else expected_value,
ev,
shared['get_result'],
"Downloaded wrong data"
"Downloaded wrong data\nSent (%9d): %s\nReceived (%9d): %s" % (
len(ev), clip_data(ev), len(shared['get_result']), clipped_result,
)
)

self.assertEqual({ "success": 1}, json.loads(shared['post_result']))

for p in post, get:
self.assertEquals(0, p.exitcode)
for name in 'post', 'get':
p = locals()[name]
self.assertEquals(0, p.exitcode, '%s had exit code %s' % (name, p.exitcode))

def _get_url(self, path, portno):
return "http://localhost:%d/%s" % (self.portno, path)
Expand Down

0 comments on commit 8efe4f3

Please sign in to comment.