basic sync support
currently no progress shown on large uploads/downloads
This commit is contained in:
parent
a1caa93054
commit
94463991bc
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright: Damien Elmes <anki@ichi2.net>
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import io
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.request, urllib.parse, urllib.error
|
import urllib.request, urllib.parse, urllib.error
|
||||||
@ -451,7 +451,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
|
|||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def mediaChangesZip(self):
|
def mediaChangesZip(self):
|
||||||
f = StringIO()
|
f = io.BytesIO()
|
||||||
z = zipfile.ZipFile(f, "w", compression=zipfile.ZIP_DEFLATED)
|
z = zipfile.ZipFile(f, "w", compression=zipfile.ZIP_DEFLATED)
|
||||||
|
|
||||||
fnames = []
|
fnames = []
|
||||||
@ -469,7 +469,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
|
|||||||
|
|
||||||
if csum:
|
if csum:
|
||||||
self.col.log("+media zip", fname)
|
self.col.log("+media zip", fname)
|
||||||
z.write(fname, str(c))
|
z.writestr(fname, str(c))
|
||||||
meta.append((normname, str(c)))
|
meta.append((normname, str(c)))
|
||||||
sz += os.path.getsize(fname)
|
sz += os.path.getsize(fname)
|
||||||
else:
|
else:
|
||||||
@ -485,11 +485,11 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
|
|||||||
|
|
||||||
def addFilesFromZip(self, zipData):
|
def addFilesFromZip(self, zipData):
|
||||||
"Extract zip data; true if finished."
|
"Extract zip data; true if finished."
|
||||||
f = StringIO(zipData)
|
f = io.BytesIO(zipData)
|
||||||
z = zipfile.ZipFile(f, "r")
|
z = zipfile.ZipFile(f, "r")
|
||||||
media = []
|
media = []
|
||||||
# get meta info first
|
# get meta info first
|
||||||
meta = json.loads(z.read("_meta"))
|
meta = json.loads(z.read("_meta").decode("utf8"))
|
||||||
# then loop through all files
|
# then loop through all files
|
||||||
cnt = 0
|
cnt = 0
|
||||||
for i in z.infolist():
|
for i in z.infolist():
|
||||||
|
61
anki/sync.py
61
anki/sync.py
@ -3,6 +3,7 @@
|
|||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import urllib.request, urllib.parse, urllib.error
|
import urllib.request, urllib.parse, urllib.error
|
||||||
|
import io
|
||||||
import sys
|
import sys
|
||||||
import gzip
|
import gzip
|
||||||
import random
|
import random
|
||||||
@ -553,21 +554,21 @@ class HttpSyncer(object):
|
|||||||
# support file uploading, so this is the more compatible choice.
|
# support file uploading, so this is the more compatible choice.
|
||||||
|
|
||||||
def req(self, method, fobj=None, comp=6, badAuthRaises=False):
|
def req(self, method, fobj=None, comp=6, badAuthRaises=False):
|
||||||
BOUNDARY="Anki-sync-boundary"
|
BOUNDARY=b"Anki-sync-boundary"
|
||||||
bdry = "--"+BOUNDARY
|
bdry = b"--"+BOUNDARY
|
||||||
buf = StringIO()
|
buf = io.BytesIO()
|
||||||
# post vars
|
# post vars
|
||||||
self.postVars['c'] = 1 if comp else 0
|
self.postVars['c'] = 1 if comp else 0
|
||||||
for (key, value) in list(self.postVars.items()):
|
for (key, value) in list(self.postVars.items()):
|
||||||
buf.write(bdry + "\r\n")
|
buf.write(bdry + b"\r\n")
|
||||||
buf.write(
|
buf.write(
|
||||||
'Content-Disposition: form-data; name="%s"\r\n\r\n%s\r\n' %
|
('Content-Disposition: form-data; name="%s"\r\n\r\n%s\r\n' %
|
||||||
(key, value))
|
(key, value)).encode("utf8"))
|
||||||
# payload as raw data or json
|
# payload as raw data or json
|
||||||
if fobj:
|
if fobj:
|
||||||
# header
|
# header
|
||||||
buf.write(bdry + "\r\n")
|
buf.write(bdry + b"\r\n")
|
||||||
buf.write("""\
|
buf.write(b"""\
|
||||||
Content-Disposition: form-data; name="data"; filename="data"\r\n\
|
Content-Disposition: form-data; name="data"; filename="data"\r\n\
|
||||||
Content-Type: application/octet-stream\r\n\r\n""")
|
Content-Type: application/octet-stream\r\n\r\n""")
|
||||||
# write file into buffer, optionally compressing
|
# write file into buffer, optionally compressing
|
||||||
@ -582,11 +583,11 @@ Content-Type: application/octet-stream\r\n\r\n""")
|
|||||||
tgt.close()
|
tgt.close()
|
||||||
break
|
break
|
||||||
tgt.write(data)
|
tgt.write(data)
|
||||||
buf.write('\r\n' + bdry + '--\r\n')
|
buf.write(b'\r\n' + bdry + b'--\r\n')
|
||||||
size = buf.tell()
|
size = buf.tell()
|
||||||
# connection headers
|
# connection headers
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'multipart/form-data; boundary=%s' % BOUNDARY,
|
'Content-Type': 'multipart/form-data; boundary=%s' % BOUNDARY.decode("utf8"),
|
||||||
'Content-Length': str(size),
|
'Content-Length': str(size),
|
||||||
}
|
}
|
||||||
body = buf.getvalue()
|
body = buf.getvalue()
|
||||||
@ -617,12 +618,12 @@ class RemoteServer(HttpSyncer):
|
|||||||
"Returns hkey or none if user/pw incorrect."
|
"Returns hkey or none if user/pw incorrect."
|
||||||
self.postVars = dict()
|
self.postVars = dict()
|
||||||
ret = self.req(
|
ret = self.req(
|
||||||
"hostKey", StringIO(json.dumps(dict(u=user, p=pw))),
|
"hostKey", io.BytesIO(json.dumps(dict(u=user, p=pw)).encode("utf8")),
|
||||||
badAuthRaises=False)
|
badAuthRaises=False)
|
||||||
if not ret:
|
if not ret:
|
||||||
# invalid auth
|
# invalid auth
|
||||||
return
|
return
|
||||||
self.hkey = json.loads(ret)['key']
|
self.hkey = json.loads(ret.decode("utf8"))['key']
|
||||||
return self.hkey
|
return self.hkey
|
||||||
|
|
||||||
def meta(self):
|
def meta(self):
|
||||||
@ -631,13 +632,13 @@ class RemoteServer(HttpSyncer):
|
|||||||
s=self.skey,
|
s=self.skey,
|
||||||
)
|
)
|
||||||
ret = self.req(
|
ret = self.req(
|
||||||
"meta", StringIO(json.dumps(dict(
|
"meta", io.BytesIO(json.dumps(dict(
|
||||||
v=SYNC_VER, cv="ankidesktop,%s,%s"%(anki.version, platDesc())))),
|
v=SYNC_VER, cv="ankidesktop,%s,%s"%(anki.version, platDesc()))).encode("utf8")),
|
||||||
badAuthRaises=False)
|
badAuthRaises=False)
|
||||||
if not ret:
|
if not ret:
|
||||||
# invalid auth
|
# invalid auth
|
||||||
return
|
return
|
||||||
return json.loads(ret)
|
return json.loads(ret.decode("utf8"))
|
||||||
|
|
||||||
def applyChanges(self, **kw):
|
def applyChanges(self, **kw):
|
||||||
return self._run("applyChanges", kw)
|
return self._run("applyChanges", kw)
|
||||||
@ -659,7 +660,7 @@ class RemoteServer(HttpSyncer):
|
|||||||
|
|
||||||
def _run(self, cmd, data):
|
def _run(self, cmd, data):
|
||||||
return json.loads(
|
return json.loads(
|
||||||
self.req(cmd, StringIO(json.dumps(data))))
|
self.req(cmd, io.BytesIO(json.dumps(data).encode("utf8"))).decode("utf8"))
|
||||||
|
|
||||||
# Full syncing
|
# Full syncing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
@ -707,7 +708,7 @@ class FullSyncer(HttpSyncer):
|
|||||||
return False
|
return False
|
||||||
# apply some adjustments, then upload
|
# apply some adjustments, then upload
|
||||||
self.col.beforeUpload()
|
self.col.beforeUpload()
|
||||||
if self.req("upload", open(self.col.path, "rb")) != "OK":
|
if self.req("upload", open(self.col.path, "rb")) != b"OK":
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -868,8 +869,8 @@ class RemoteMediaServer(HttpSyncer):
|
|||||||
k=self.hkey,
|
k=self.hkey,
|
||||||
v="ankidesktop,%s,%s"%(anki.version, platDesc())
|
v="ankidesktop,%s,%s"%(anki.version, platDesc())
|
||||||
)
|
)
|
||||||
ret = self._dataOnly(json.loads(self.req(
|
ret = self._dataOnly(self.req(
|
||||||
"begin", StringIO(json.dumps(dict())))))
|
"begin", io.BytesIO(json.dumps(dict()).encode("utf8"))))
|
||||||
self.skey = ret['sk']
|
self.skey = ret['sk']
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@ -878,25 +879,25 @@ class RemoteMediaServer(HttpSyncer):
|
|||||||
self.postVars = dict(
|
self.postVars = dict(
|
||||||
sk=self.skey,
|
sk=self.skey,
|
||||||
)
|
)
|
||||||
resp = json.loads(
|
return self._dataOnly(
|
||||||
self.req("mediaChanges", StringIO(json.dumps(kw))))
|
self.req("mediaChanges", io.BytesIO(json.dumps(kw).encode("utf8"))))
|
||||||
return self._dataOnly(resp)
|
|
||||||
|
|
||||||
# args: files
|
# args: files
|
||||||
def downloadFiles(self, **kw):
|
def downloadFiles(self, **kw):
|
||||||
return self.req("downloadFiles", StringIO(json.dumps(kw)))
|
return self.req("downloadFiles", io.BytesIO(json.dumps(kw).encode("utf8")))
|
||||||
|
|
||||||
def uploadChanges(self, zip):
|
def uploadChanges(self, zip):
|
||||||
# no compression, as we compress the zip file instead
|
# no compression, as we compress the zip file instead
|
||||||
return self._dataOnly(json.loads(
|
return self._dataOnly(
|
||||||
self.req("uploadChanges", StringIO(zip), comp=0)))
|
self.req("uploadChanges", io.BytesIO(zip), comp=0))
|
||||||
|
|
||||||
# args: local
|
# args: local
|
||||||
def mediaSanity(self, **kw):
|
def mediaSanity(self, **kw):
|
||||||
return self._dataOnly(json.loads(
|
return self._dataOnly(
|
||||||
self.req("mediaSanity", StringIO(json.dumps(kw)))))
|
self.req("mediaSanity", io.BytesIO(json.dumps(kw).encode("utf8"))))
|
||||||
|
|
||||||
def _dataOnly(self, resp):
|
def _dataOnly(self, resp):
|
||||||
|
resp = json.loads(resp.decode("utf8"))
|
||||||
if resp['err']:
|
if resp['err']:
|
||||||
self.col.log("error returned:%s"%resp['err'])
|
self.col.log("error returned:%s"%resp['err'])
|
||||||
raise Exception("SyncError:%s"%resp['err'])
|
raise Exception("SyncError:%s"%resp['err'])
|
||||||
@ -907,6 +908,6 @@ class RemoteMediaServer(HttpSyncer):
|
|||||||
self.postVars = dict(
|
self.postVars = dict(
|
||||||
k=self.hkey,
|
k=self.hkey,
|
||||||
)
|
)
|
||||||
return self._dataOnly(json.loads(
|
return self._dataOnly(
|
||||||
self.req("newMediaTest", StringIO(
|
self.req("newMediaTest", io.BytesIO(
|
||||||
json.dumps(dict(cmd=cmd))))))
|
json.dumps(dict(cmd=cmd)).encode("utf8"))))
|
||||||
|
@ -576,9 +576,6 @@ title="%s" %s>%s</button>''' % (
|
|||||||
if not auto or (self.pm.profile['syncKey'] and
|
if not auto or (self.pm.profile['syncKey'] and
|
||||||
self.pm.profile['autoSync'] and
|
self.pm.profile['autoSync'] and
|
||||||
not self.safeMode):
|
not self.safeMode):
|
||||||
tooltip("Syncing not yet implemented")
|
|
||||||
return
|
|
||||||
|
|
||||||
from aqt.sync import SyncManager
|
from aqt.sync import SyncManager
|
||||||
if not self.unloadCollection():
|
if not self.unloadCollection():
|
||||||
return
|
return
|
||||||
|
Loading…
Reference in New Issue
Block a user