2012-12-21 08:51:59 +01:00
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
2013-10-10 04:48:38 +02:00
import time
import traceback
import gc
2012-12-21 08:51:59 +01:00
from aqt . qt import *
import aqt
from anki import Collection
from anki . sync import Syncer , RemoteServer , FullSyncer , MediaSyncer , \
RemoteMediaServer
from anki . hooks import addHook , remHook
2013-02-28 17:36:14 +01:00
from aqt . utils import tooltip , askUserDialog , showWarning , showText , showInfo
2012-12-21 08:51:59 +01:00
2013-10-10 04:48:38 +02:00
2012-12-21 08:51:59 +01:00
# Sync manager
######################################################################
class SyncManager ( QObject ) :
def __init__ ( self , mw , pm ) :
QObject . __init__ ( self , mw )
self . mw = mw
self . pm = pm
def sync ( self ) :
if not self . pm . profile [ ' syncKey ' ] :
auth = self . _getUserPass ( )
if not auth :
return
2013-10-03 23:09:28 +02:00
self . pm . profile [ ' syncUser ' ] = auth [ 0 ]
2012-12-21 08:51:59 +01:00
self . _sync ( auth )
else :
self . _sync ( )
def _sync ( self , auth = None ) :
# to avoid gui widgets being garbage collected in the worker thread,
# run gc in advance
2013-02-27 06:07:11 +01:00
self . _didFullUp = False
2013-02-28 17:36:14 +01:00
self . _didError = False
2012-12-21 08:51:59 +01:00
gc . collect ( )
# create the thread, setup signals and start running
t = self . thread = SyncThread (
self . pm . collectionPath ( ) , self . pm . profile [ ' syncKey ' ] ,
auth = auth , media = self . pm . profile [ ' syncMedia ' ] )
2016-05-31 10:51:40 +02:00
t . event . connect ( self . onEvent )
2012-12-21 08:51:59 +01:00
self . label = _ ( " Connecting... " )
2017-12-28 09:31:05 +01:00
prog = self . mw . progress . start ( immediate = True , label = self . label )
2012-12-21 08:51:59 +01:00
self . sentBytes = self . recvBytes = 0
self . _updateLabel ( )
self . thread . start ( )
while not self . thread . isFinished ( ) :
2017-12-28 09:31:05 +01:00
if prog . wantCancel :
2017-01-17 08:15:50 +01:00
self . thread . flagAbort ( )
# make sure we don't display 'upload success' msg
self . _didFullUp = False
# abort may take a while
self . mw . progress . update ( _ ( " Stopping... " ) )
2012-12-21 08:51:59 +01:00
self . mw . app . processEvents ( )
self . thread . wait ( 100 )
self . mw . progress . finish ( )
2013-10-03 23:09:28 +02:00
if self . thread . syncMsg :
showText ( self . thread . syncMsg )
if self . thread . uname :
self . pm . profile [ ' syncUser ' ] = self . thread . uname
2013-02-28 17:36:14 +01:00
def delayedInfo ( ) :
if self . _didFullUp and not self . _didError :
showInfo ( _ ( """ \
2013-02-27 06:07:11 +01:00
Your collection was successfully uploaded to AnkiWeb .
If you use any other devices , please sync them now , and choose \
to download the collection you have just uploaded from this computer . \
After doing so , future reviews and added cards will be merged \
automatically . """ ))
2013-02-28 17:36:14 +01:00
self . mw . progress . timer ( 1000 , delayedInfo , False )
2012-12-21 08:51:59 +01:00
def _updateLabel ( self ) :
self . mw . progress . update ( label = " %s \n %s " % (
self . label ,
2017-01-08 10:06:32 +01:00
_ ( " %(a)0.1f kB up, %(b)0.1f kB down " ) % dict (
a = self . sentBytes / 1024 ,
b = self . recvBytes / 1024 ) ) )
2012-12-21 08:51:59 +01:00
def onEvent ( self , evt , * args ) :
pu = self . mw . progress . update
if evt == " badAuth " :
tooltip (
_ ( " AnkiWeb ID or password was incorrect; please try again. " ) ,
parent = self . mw )
# blank the key so we prompt user again
self . pm . profile [ ' syncKey ' ] = None
self . pm . save ( )
elif evt == " corrupt " :
pass
elif evt == " newKey " :
self . pm . profile [ ' syncKey ' ] = args [ 0 ]
self . pm . save ( )
elif evt == " offline " :
tooltip ( _ ( " Syncing failed; internet offline. " ) )
2013-05-13 11:48:37 +02:00
elif evt == " upbad " :
self . _didFullUp = False
2013-05-14 08:41:10 +02:00
self . _checkFailed ( )
2012-12-21 08:51:59 +01:00
elif evt == " sync " :
m = None ; t = args [ 0 ]
if t == " login " :
m = _ ( " Syncing... " )
elif t == " upload " :
2013-02-27 06:07:11 +01:00
self . _didFullUp = True
2012-12-21 08:51:59 +01:00
m = _ ( " Uploading to AnkiWeb... " )
elif t == " download " :
m = _ ( " Downloading from AnkiWeb... " )
elif t == " sanity " :
m = _ ( " Checking... " )
elif t == " findMedia " :
2017-11-27 02:21:03 +01:00
m = _ ( " Checking media... " )
2012-12-21 08:51:59 +01:00
elif t == " upgradeRequired " :
showText ( _ ( """ \
Please visit AnkiWeb , upgrade your deck , then try again . """ ))
if m :
self . label = m
self . _updateLabel ( )
2014-07-28 10:00:26 +02:00
elif evt == " syncMsg " :
self . label = args [ 0 ]
self . _updateLabel ( )
2012-12-21 08:51:59 +01:00
elif evt == " error " :
2013-02-28 17:36:14 +01:00
self . _didError = True
2012-12-21 08:51:59 +01:00
showText ( _ ( " Syncing failed: \n %s " ) %
self . _rewriteError ( args [ 0 ] ) )
elif evt == " clockOff " :
self . _clockOff ( )
2013-05-14 08:41:10 +02:00
elif evt == " checkFailed " :
self . _checkFailed ( )
2013-05-30 03:54:55 +02:00
elif evt == " mediaSanity " :
showWarning ( _ ( """ \
2013-11-14 04:12:37 +01:00
A problem occurred while syncing media . Please use Tools > Check Media , then \
sync again to correct the issue . """ ))
2012-12-21 08:51:59 +01:00
elif evt == " noChanges " :
pass
elif evt == " fullSync " :
self . _confirmFullSync ( )
elif evt == " send " :
# posted events not guaranteed to arrive in order
2016-05-31 10:51:40 +02:00
self . sentBytes = max ( self . sentBytes , int ( args [ 0 ] ) )
2012-12-21 08:51:59 +01:00
self . _updateLabel ( )
elif evt == " recv " :
2016-05-31 10:51:40 +02:00
self . recvBytes = max ( self . recvBytes , int ( args [ 0 ] ) )
2012-12-21 08:51:59 +01:00
self . _updateLabel ( )
def _rewriteError ( self , err ) :
if " Errno 61 " in err :
return _ ( """ \
Couldn ' t connect to AnkiWeb. Please check your network connection \
and try again . """ )
elif " timed out " in err or " 10060 " in err :
return _ ( """ \
The connection to AnkiWeb timed out . Please check your network \
connection and try again . """ )
2013-10-14 04:18:52 +02:00
elif " code: 500 " in err :
2012-12-21 08:51:59 +01:00
return _ ( """ \
AnkiWeb encountered an error . Please try again in a few minutes , and if \
the problem persists , please file a bug report . """ )
2013-10-14 04:18:52 +02:00
elif " code: 501 " in err :
2012-12-21 08:51:59 +01:00
return _ ( """ \
Please upgrade to the latest version of Anki . """ )
# 502 is technically due to the server restarting, but we reuse the
# error message
2013-10-14 04:18:52 +02:00
elif " code: 502 " in err :
2013-05-17 06:58:38 +02:00
return _ ( " AnkiWeb is under maintenance. Please try again in a few minutes. " )
2013-10-14 04:18:52 +02:00
elif " code: 503 " in err :
2012-12-21 08:51:59 +01:00
return _ ( """ \
AnkiWeb is too busy at the moment . Please try again in a few minutes . """ )
2013-10-14 04:18:52 +02:00
elif " code: 504 " in err :
2013-05-17 06:58:38 +02:00
return _ ( " 504 gateway timeout error received. Please try temporarily disabling your antivirus. " )
2013-10-14 04:18:52 +02:00
elif " code: 409 " in err :
2013-10-10 04:48:38 +02:00
return _ ( " Only one client can access AnkiWeb at a time. If a previous sync failed, please try again in a few minutes. " )
2013-11-27 15:02:03 +01:00
elif " 10061 " in err or " 10013 " in err or " 10053 " in err :
2012-12-21 08:51:59 +01:00
return _ (
" Antivirus or firewall software is preventing Anki from connecting to the internet. " )
2014-01-23 19:10:04 +01:00
elif " 10054 " in err or " Broken pipe " in err :
2014-01-14 06:12:49 +01:00
return _ ( " Connection timed out. Either your internet connection is experiencing problems, or you have a very large file in your media folder. " )
2013-02-28 17:36:14 +01:00
elif " Unable to find the server " in err :
2013-02-11 18:06:03 +01:00
return _ (
" Server not found. Either your connection is down, or antivirus/firewall "
" software is blocking Anki from connecting to the internet. " )
2013-10-14 04:18:52 +02:00
elif " code: 407 " in err :
2012-12-21 08:51:59 +01:00
return _ ( " Proxy authentication required. " )
2013-10-14 04:18:52 +02:00
elif " code: 413 " in err :
return _ ( " Your collection or a media file is too large to sync. " )
2014-02-05 22:50:41 +01:00
elif " EOF occurred in violation of protocol " in err :
2017-12-07 08:15:20 +01:00
return _ ( " Error establishing a secure connection. This is usually caused by antivirus, firewall or VPN software, or problems with your ISP. " ) + " (eof) "
2014-09-01 14:06:41 +02:00
elif " certificate verify failed " in err :
2017-12-07 08:15:20 +01:00
return _ ( " Error establishing a secure connection. This is usually caused by antivirus, firewall or VPN software, or problems with your ISP. " ) + " (invalid cert) "
2012-12-21 08:51:59 +01:00
return err
def _getUserPass ( self ) :
d = QDialog ( self . mw )
d . setWindowTitle ( " Anki " )
2013-01-08 02:36:04 +01:00
d . setWindowModality ( Qt . WindowModal )
2012-12-21 08:51:59 +01:00
vbox = QVBoxLayout ( )
l = QLabel ( _ ( """ \
< h1 > Account Required < / h1 >
A free account is required to keep your collection synchronized . Please \
< a href = " %s " > sign up < / a > for an account , then \
enter your details below . """ ) %
" https://ankiweb.net/account/login " )
l . setOpenExternalLinks ( True )
l . setWordWrap ( True )
vbox . addWidget ( l )
vbox . addSpacing ( 20 )
g = QGridLayout ( )
l1 = QLabel ( _ ( " AnkiWeb ID: " ) )
g . addWidget ( l1 , 0 , 0 )
user = QLineEdit ( )
g . addWidget ( user , 0 , 1 )
l2 = QLabel ( _ ( " Password: " ) )
g . addWidget ( l2 , 1 , 0 )
passwd = QLineEdit ( )
passwd . setEchoMode ( QLineEdit . Password )
g . addWidget ( passwd , 1 , 1 )
vbox . addLayout ( g )
bb = QDialogButtonBox ( QDialogButtonBox . Ok | QDialogButtonBox . Cancel )
bb . button ( QDialogButtonBox . Ok ) . setAutoDefault ( True )
2016-05-31 10:51:40 +02:00
bb . accepted . connect ( d . accept )
bb . rejected . connect ( d . reject )
2012-12-21 08:51:59 +01:00
vbox . addWidget ( bb )
d . setLayout ( vbox )
d . show ( )
2014-06-24 22:04:23 +02:00
accepted = d . exec_ ( )
2012-12-21 08:51:59 +01:00
u = user . text ( )
p = passwd . text ( )
2014-06-24 22:04:23 +02:00
if not accepted or not u or not p :
2012-12-21 08:51:59 +01:00
return
return ( u , p )
def _confirmFullSync ( self ) :
2017-01-08 13:21:58 +01:00
if self . thread . localIsEmpty :
diag = askUserDialog (
_ ( " Local collection has no cards. Download from AnkiWeb? " ) ,
[ _ ( " Download from AnkiWeb " ) , _ ( " Cancel " ) ] )
diag . setDefault ( 1 )
else :
diag = askUserDialog ( _ ( """ \
2012-12-21 08:51:59 +01:00
Your decks here and on AnkiWeb differ in such a way that they can ' t \
be merged together , so it ' s necessary to overwrite the decks on one \
side with the decks from the other .
2013-02-27 06:07:11 +01:00
If you choose download , Anki will download the collection from AnkiWeb , \
and any changes you have made on your computer since the last sync will \
be lost .
If you choose upload , Anki will upload your collection to AnkiWeb , and \
any changes you have made on AnkiWeb or your other devices since the \
last sync to this device will be lost .
2013-02-27 11:36:15 +01:00
After all devices are in sync , future reviews and added cards can be merged \
2013-02-27 06:07:11 +01:00
automatically . """ ),
2012-12-21 08:51:59 +01:00
[ _ ( " Upload to AnkiWeb " ) ,
_ ( " Download from AnkiWeb " ) ,
_ ( " Cancel " ) ] )
2017-01-08 13:21:58 +01:00
diag . setDefault ( 2 )
2012-12-21 08:51:59 +01:00
ret = diag . run ( )
if ret == _ ( " Upload to AnkiWeb " ) :
self . thread . fullSyncChoice = " upload "
elif ret == _ ( " Download from AnkiWeb " ) :
self . thread . fullSyncChoice = " download "
else :
self . thread . fullSyncChoice = " cancel "
def _clockOff ( self ) :
showWarning ( _ ( """ \
Syncing requires the clock on your computer to be set correctly . Please \
fix the clock and try again . """ ))
2013-05-14 08:41:10 +02:00
def _checkFailed ( self ) :
2013-05-14 08:10:58 +02:00
showWarning ( _ ( """ \
Your collection is in an inconsistent state . Please run Tools > \
2013-05-14 08:41:10 +02:00
Check Database , then sync again . """ ))
2013-05-14 08:10:58 +02:00
2012-12-21 08:51:59 +01:00
def badUserPass ( self ) :
aqt . preferences . Preferences ( self , self . pm . profile ) . dialog . tabWidget . \
setCurrentIndex ( 1 )
# Sync thread
######################################################################
class SyncThread ( QThread ) :
2016-05-31 10:51:40 +02:00
event = pyqtSignal ( str , str )
2012-12-21 08:51:59 +01:00
def __init__ ( self , path , hkey , auth = None , media = True ) :
QThread . __init__ ( self )
self . path = path
self . hkey = hkey
self . auth = auth
self . media = media
2017-01-17 08:15:50 +01:00
self . _abort = 0 # 1=flagged, 2=aborting
def flagAbort ( self ) :
self . _abort = 1
2012-12-21 08:51:59 +01:00
def run ( self ) :
2013-10-22 08:30:46 +02:00
# init this first so an early crash doesn't cause an error
# in the main thread
self . syncMsg = " "
self . uname = " "
2012-12-21 08:51:59 +01:00
try :
2013-11-13 08:48:22 +01:00
self . col = Collection ( self . path , log = True )
2012-12-21 08:51:59 +01:00
except :
self . fireEvent ( " corrupt " )
return
self . server = RemoteServer ( self . hkey )
self . client = Syncer ( self . col , self . server )
self . sentTotal = 0
self . recvTotal = 0
def syncEvent ( type ) :
self . fireEvent ( " sync " , type )
2014-07-28 10:00:26 +02:00
def syncMsg ( msg ) :
self . fireEvent ( " syncMsg " , msg )
2012-12-21 08:51:59 +01:00
def sendEvent ( bytes ) :
2017-01-17 08:15:50 +01:00
if not self . _abort :
self . sentTotal + = bytes
self . fireEvent ( " send " , str ( self . sentTotal ) )
elif self . _abort == 1 :
self . _abort = 2
raise Exception ( " sync cancelled " )
2012-12-21 08:51:59 +01:00
def recvEvent ( bytes ) :
2017-01-17 08:15:50 +01:00
if not self . _abort :
self . recvTotal + = bytes
self . fireEvent ( " recv " , str ( self . recvTotal ) )
elif self . _abort == 1 :
self . _abort = 2
raise Exception ( " sync cancelled " )
2012-12-21 08:51:59 +01:00
addHook ( " sync " , syncEvent )
2014-07-28 10:00:26 +02:00
addHook ( " syncMsg " , syncMsg )
2012-12-21 08:51:59 +01:00
addHook ( " httpSend " , sendEvent )
addHook ( " httpRecv " , recvEvent )
# run sync and catch any errors
try :
self . _sync ( )
except :
err = traceback . format_exc ( )
self . fireEvent ( " error " , err )
finally :
# don't bump mod time unless we explicitly save
self . col . close ( save = False )
remHook ( " sync " , syncEvent )
2014-07-28 10:00:26 +02:00
remHook ( " syncMsg " , syncMsg )
2012-12-21 08:51:59 +01:00
remHook ( " httpSend " , sendEvent )
remHook ( " httpRecv " , recvEvent )
2017-01-17 08:15:50 +01:00
def _abortingSync ( self ) :
try :
return self . client . sync ( )
except Exception as e :
if " sync cancelled " in str ( e ) :
self . server . abort ( )
raise
else :
raise
2012-12-21 08:51:59 +01:00
def _sync ( self ) :
if self . auth :
# need to authenticate and obtain host key
self . hkey = self . server . hostKey ( * self . auth )
if not self . hkey :
# provided details were invalid
return self . fireEvent ( " badAuth " )
else :
# write new details and tell calling thread to save
self . fireEvent ( " newKey " , self . hkey )
# run sync and check state
try :
2017-01-17 08:15:50 +01:00
ret = self . _abortingSync ( )
2016-05-12 06:45:35 +02:00
except Exception as e :
2012-12-21 08:51:59 +01:00
log = traceback . format_exc ( )
2013-11-14 03:14:21 +01:00
err = repr ( str ( e ) )
if ( " Unable to find the server " in err or
2017-08-15 10:46:07 +02:00
" Errno 2 " in err or " getaddrinfo " in err ) :
2012-12-21 08:51:59 +01:00
self . fireEvent ( " offline " )
2017-01-17 08:15:50 +01:00
elif " sync cancelled " in err :
pass
2012-12-21 08:51:59 +01:00
else :
2013-01-14 23:28:39 +01:00
if not err :
err = log
2013-06-14 06:06:56 +02:00
self . fireEvent ( " error " , err )
2012-12-21 08:51:59 +01:00
return
if ret == " badAuth " :
return self . fireEvent ( " badAuth " )
elif ret == " clockOff " :
return self . fireEvent ( " clockOff " )
2013-05-14 08:41:10 +02:00
elif ret == " basicCheckFailed " or ret == " sanityCheckFailed " :
return self . fireEvent ( " checkFailed " )
2012-12-21 08:51:59 +01:00
# full sync?
if ret == " fullSync " :
return self . _fullSync ( )
# save and note success state
if ret == " noChanges " :
self . fireEvent ( " noChanges " )
2013-10-03 23:09:28 +02:00
elif ret == " success " :
2012-12-21 08:51:59 +01:00
self . fireEvent ( " success " )
2013-10-03 23:09:28 +02:00
elif ret == " serverAbort " :
2015-03-12 02:54:49 +01:00
pass
2013-10-03 23:09:28 +02:00
else :
self . fireEvent ( " error " , " Unknown sync return code. " )
self . syncMsg = self . client . syncMsg
self . uname = self . client . uname
2012-12-21 08:51:59 +01:00
# then move on to media sync
self . _syncMedia ( )
def _fullSync ( self ) :
2017-01-08 13:21:58 +01:00
# tell the calling thread we need a decision on sync direction, and
# wait for a reply
self . fullSyncChoice = False
self . localIsEmpty = self . col . isEmpty ( )
self . fireEvent ( " fullSync " )
while not self . fullSyncChoice :
time . sleep ( 0.1 )
f = self . fullSyncChoice
2012-12-21 08:51:59 +01:00
if f == " cancel " :
return
2017-01-08 10:06:32 +01:00
self . client = FullSyncer ( self . col , self . hkey , self . server . client )
2017-01-17 08:15:50 +01:00
try :
if f == " upload " :
if not self . client . upload ( ) :
self . fireEvent ( " upbad " )
else :
self . client . download ( )
except Exception as e :
if " sync cancelled " in str ( e ) :
return
raise
2012-12-21 08:51:59 +01:00
# reopen db and move on to media sync
self . col . reopen ( )
self . _syncMedia ( )
def _syncMedia ( self ) :
if not self . media :
return
2017-01-08 10:06:32 +01:00
self . server = RemoteMediaServer ( self . col , self . hkey , self . server . client )
2012-12-21 08:51:59 +01:00
self . client = MediaSyncer ( self . col , self . server )
2017-01-17 08:15:50 +01:00
try :
ret = self . client . sync ( )
except Exception as e :
if " sync cancelled " in str ( e ) :
return
raise
2012-12-21 08:51:59 +01:00
if ret == " noChanges " :
self . fireEvent ( " noMediaChanges " )
2017-08-17 05:33:54 +02:00
elif ret == " sanityCheckFailed " or ret == " corruptMediaDB " :
2013-05-30 03:54:55 +02:00
self . fireEvent ( " mediaSanity " )
2012-12-21 08:51:59 +01:00
else :
self . fireEvent ( " mediaSuccess " )
2016-05-31 10:51:40 +02:00
def fireEvent ( self , cmd , arg = " " ) :
self . event . emit ( cmd , arg )
2012-12-21 08:51:59 +01:00