factor out file uploads; use for incremental sync too

This commit is contained in:
Damien Elmes 2011-09-30 21:08:52 +09:00
parent 20d753591d
commit dd5b2056fb

View file

@ -15,7 +15,7 @@ from hooks import runHook
if simplejson.__version__ < "1.7.3": if simplejson.__version__ < "1.7.3":
raise Exception("SimpleJSON must be 1.7.3 or later.") raise Exception("SimpleJSON must be 1.7.3 or later.")
CHUNK_SIZE = 32768 CHUNK_SIZE = 65536
MIME_BOUNDARY = "Anki-sync-boundary" MIME_BOUNDARY = "Anki-sync-boundary"
SYNC_HOST = os.environ.get("SYNC_HOST") or "dev.ankiweb.net" SYNC_HOST = os.environ.get("SYNC_HOST") or "dev.ankiweb.net"
SYNC_PORT = int(os.environ.get("SYNC_PORT") or 80) SYNC_PORT = int(os.environ.get("SYNC_PORT") or 80)
@ -420,28 +420,17 @@ class RemoteServer(Syncer):
self.hkey = cont self.hkey = cont
return cont return cont
def gzipped(self, data):
buf = StringIO()
fn = gzip.GzipFile(mode="wb", fileobj=buf)
fn.write(data)
fn.close()
res = buf.getvalue()
return res
def applyChanges(self, **kwargs): def applyChanges(self, **kwargs):
self.con = httplib2.Http(timeout=60) self.con = httplib2.Http(timeout=60)
return self._run("applyChanges", kwargs) return self._run("applyChanges", kwargs)
def _vars(self):
return dict(k=self.hkey)
def _run(self, cmd, data): def _run(self, cmd, data):
data['k'] = self.hkey return simplejson.loads(
data = self.gzipped(simplejson.dumps(data)) postData(self.con, cmd, StringIO(simplejson.dumps(data)),
data = urllib.urlencode(dict(data=data)) self._vars()))
headers = {'Content-Type': 'application/octet-stream'}
resp, cont = self.con.request(SYNC_URL+cmd, "POST", body=data,
headers=headers)
if resp['status'] != '200':
raise Exception("Invalid response code: %s" % resp['status'])
return simplejson.loads(cont)
# Full syncing # Full syncing
########################################################################## ##########################################################################
@ -452,11 +441,16 @@ class FullSyncer(object):
self.deck = deck self.deck = deck
self.hkey = hkey self.hkey = hkey
def _vars(self):
return dict(k=self.hkey)
def _con(self):
return httplib2.Http(timeout=60)
def download(self): def download(self):
self.deck.close() self.deck.close()
h = httplib2.Http(timeout=60) resp, cont = self._con().request(
resp, cont = h.request( SYNC_URL+"download?" + urllib.urlencode(self._vars()))
SYNC_URL+"download?" + urllib.urlencode(dict(k=self.hkey)))
if resp['status'] != '200': if resp['status'] != '200':
raise Exception("Invalid response code: %s" % resp['status']) raise Exception("Invalid response code: %s" % resp['status'])
tpath = self.deck.path + ".tmp" tpath = self.deck.path + ".tmp"
@ -469,43 +463,51 @@ class FullSyncer(object):
def upload(self): def upload(self):
self.deck.beforeUpload() self.deck.beforeUpload()
# compressed post body support is flaky, so bundle into a zip assert postData(self._con(), "upload", open(self.deck.path, "rb"),
f = StringIO() self._vars(), comp=6) == "OK"
z = zipfile.ZipFile(f, mode="w", compression=zipfile.ZIP_DEFLATED)
z.write(self.deck.path, "col.anki") # We don't want to post the payload as a form var, as the percent-encoding is
z.close() # costly. We could send it as a raw post, but more HTTP clients seem to
# build an upload body # support file uploading, so this is the more compatible choice.
f2 = StringIO() def postData(http, method, fobj, vars, comp=1):
fields = dict(k=self.hkey) bdry = "--"+MIME_BOUNDARY
# post vars # write out post vars, including session key and compression flag
for (key, value) in fields.items(): buf = StringIO()
f2.write('--' + MIME_BOUNDARY + "\r\n") vars = vars or {}
f2.write('Content-Disposition: form-data; name="%s"\r\n' % key) vars['c'] = 1 if comp else 0
f2.write('\r\n') for (key, value) in vars.items():
f2.write(value) buf.write(bdry + "\r\n")
f2.write('\r\n') buf.write(
'Content-Disposition: form-data; name="%s"\r\n\r\n%s\r\n' %
(key, value))
# file header # file header
f2.write('--' + MIME_BOUNDARY + "\r\n") buf.write(bdry + "\r\n")
f2.write( buf.write("""\
'Content-Disposition: form-data; name="deck"; filename="deck"\r\n') Content-Disposition: form-data; name="data"; filename="data"\r\n\
f2.write('Content-Type: application/octet-stream\r\n') Content-Type: application/octet-stream\r\n\r\n""")
f2.write('\r\n') # write file into buffer, optionally compressing
f2.write(f.getvalue()) if comp:
f.close() tgt = gzip.GzipFile(mode="wb", fileobj=buf, compresslevel=comp)
f2.write('\r\n--' + MIME_BOUNDARY + '--\r\n\r\n') else:
size = f2.tell() tgt = buf
while 1:
data = fobj.read(CHUNK_SIZE)
if not data:
if comp:
tgt.close()
break
tgt.write(data)
buf.write('\r\n' + bdry + '--\r\n')
size = buf.tell()
# connection headers # connection headers
headers = { headers = {
'Content-type': 'multipart/form-data; boundary=%s' % 'Content-Type': 'multipart/form-data; boundary=%s' % MIME_BOUNDARY,
MIME_BOUNDARY, 'Content-Length': str(size),
'Content-length': str(size),
'Host': SYNC_HOST,
} }
body = f2.getvalue() body = buf.getvalue()
f2.close() buf.close()
h = httplib2.Http(timeout=60) resp, cont = http.request(
resp, cont = h.request( SYNC_URL+method, "POST", headers=headers, body=body)
SYNC_URL+"upload", "POST", headers=headers, body=body)
if resp['status'] != '200': if resp['status'] != '200':
raise Exception("Invalid response code: %s" % resp['status']) raise Exception("Invalid response code: %s" % resp['status'])
assert cont == "OK" return cont