# Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import gc import time from anki import hooks from anki.lang import _ from anki.storage import Collection from anki.sync import FullSyncer, RemoteServer, Syncer from aqt.qt import * from aqt.utils import askUserDialog, showInfo, showText, showWarning, tooltip # 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 self.pm.profile["syncUser"] = auth[0] 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 self._didFullUp = False self._didError = False gc.collect() # create the thread, setup signals and start running t = self.thread = SyncThread( self.pm.collectionPath(), self.pm.profile["syncKey"], auth=auth, hostNum=self.pm.profile.get("hostNum"), ) qconnect(t._event, self.onEvent) qconnect(t.progress_event, self.on_progress) self.label = _("Connecting...") prog = self.mw.progress.start(immediate=True, label=self.label) self.sentBytes = self.recvBytes = 0 self._updateLabel() self.thread.start() while not self.thread.isFinished(): if prog.wantCancel: 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...")) self.mw.app.processEvents() self.thread.wait(100) self.mw.progress.finish() if self.thread.syncMsg: showText(self.thread.syncMsg) if self.thread.uname: self.pm.profile["syncUser"] = self.thread.uname self.pm.profile["hostNum"] = self.thread.hostNum def delayedInfo(): if self._didFullUp and not self._didError: showInfo( _( """\ 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.""" ) ) self.mw.progress.timer(1000, delayedInfo, False, requiresCollection=False) def _updateLabel(self): self.mw.progress.update( label="%s\n%s" % ( self.label, _("%(a)0.1fkB up, %(b)0.1fkB down") % dict(a=self.sentBytes / 1024, b=self.recvBytes / 1024), ) ) def on_progress(self, upload: int, download: int) -> None: # posted events not guaranteed to arrive in order; don't go backwards self.sentBytes = max(self.sentBytes, upload) self.recvBytes = max(self.recvBytes, download) self._updateLabel() 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.")) elif evt == "upbad": self._didFullUp = False self._checkFailed() elif evt == "sync": m = None t = args[0] if t == "login": m = _("Syncing...") elif t == "upload": self._didFullUp = True m = _("Uploading to AnkiWeb...") elif t == "download": m = _("Downloading from AnkiWeb...") elif t == "sanity": m = _("Checking...") elif t == "upgradeRequired": showText( _( """\ Please visit AnkiWeb, upgrade your deck, then try again.""" ) ) if m: self.label = m self._updateLabel() elif evt == "syncMsg": self.label = args[0] self._updateLabel() elif evt == "error": self._didError = True showText(_("Syncing failed:\n%s") % self._rewriteError(args[0])) elif evt == "clockOff": self._clockOff() elif evt == "checkFailed": self._checkFailed() elif evt == "noChanges": pass elif evt == "fullSync": self._confirmFullSync() elif evt == "downloadClobber": showInfo( _( "Your AnkiWeb collection does not contain any cards. Please sync again and choose 'Upload' instead." ) ) 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.""" ) elif "code: 500" in err: return _( """\ AnkiWeb encountered an error. Please try again in a few minutes, and if \ the problem persists, please file a bug report.""" ) elif "code: 501" in err: return _( """\ Please upgrade to the latest version of Anki.""" ) # 502 is technically due to the server restarting, but we reuse the # error message elif "code: 502" in err: return _("AnkiWeb is under maintenance. Please try again in a few minutes.") elif "code: 503" in err: return _( """\ AnkiWeb is too busy at the moment. Please try again in a few minutes.""" ) elif "code: 504" in err: return _( "504 gateway timeout error received. Please try temporarily disabling your antivirus." ) elif "code: 409" in err: return _( "Only one client can access AnkiWeb at a time. If a previous sync failed, please try again in a few minutes." ) elif "10061" in err or "10013" in err or "10053" in err: return _( "Antivirus or firewall software is preventing Anki from connecting to the internet." ) elif "10054" in err or "Broken pipe" in err: return _( "Connection timed out. Either your internet connection is experiencing problems, or you have a very large file in your media folder." ) elif "Unable to find the server" in err or "socket.gaierror" in err: return _( "Server not found. Either your connection is down, or antivirus/firewall " "software is blocking Anki from connecting to the internet." ) elif "code: 407" in err: return _("Proxy authentication required.") elif "code: 413" in err: return _("Your collection or a media file is too large to sync.") elif "EOF occurred in violation of protocol" in err: return ( _( "Error establishing a secure connection. This is usually caused by antivirus, firewall or VPN software, or problems with your ISP." ) + " (eof)" ) elif "certificate verify failed" in err: return ( _( "Error establishing a secure connection. This is usually caused by antivirus, firewall or VPN software, or problems with your ISP." ) + " (invalid cert)" ) return err def _getUserPass(self): d = QDialog(self.mw) d.setWindowTitle("Anki") d.setWindowModality(Qt.WindowModal) vbox = QVBoxLayout() l = QLabel( _( """\