move more logic into mediasync.py, handle auth errors

This commit is contained in:
Damien Elmes 2020-02-04 12:26:10 +10:00
parent 347ac80086
commit 93c768cab9
3 changed files with 69 additions and 75 deletions

View File

@ -84,7 +84,7 @@ class AnkiQt(QMainWindow):
self.opts = opts
self.col: Optional[_Collection] = None
self.taskman = TaskManager()
self.media_syncer = MediaSyncer(self.taskman, self._on_media_sync_start_stop)
self.media_syncer = MediaSyncer(self)
aqt.mw = self
self.app = app
self.pm = profileManager
@ -833,7 +833,7 @@ title="%s" %s>%s</button>""" % (
# collection after sync completes
def onSync(self):
if self.media_syncer.is_syncing():
self._show_sync_log()
self.media_syncer.show_sync_log()
else:
self.unloadCollection(self._onSync)
@ -841,7 +841,7 @@ title="%s" %s>%s</button>""" % (
self._sync()
if not self.loadCollection():
return
self._sync_media()
self.media_syncer.start()
# expects a current profile, but no collection loaded
def maybeAutoSync(self) -> None:
@ -863,31 +863,6 @@ title="%s" %s>%s</button>""" % (
self.syncer = SyncManager(self, self.pm)
self.syncer.sync()
# fixme: self.pm.profile["syncMedia"]
# fixme: mediaSanity
# fixme: corruptMediaDB
# fixme: hkey
# fixme: shard
# fixme: dialog
# fixme: autosync
# elif evt == "mediaSanity":
# showWarning(
# _(
# """\
# A problem occurred while syncing media. Please use Tools>Check Media, then \
# sync again to correct the issue."""
# )
# )
def _sync_media(self):
self.media_syncer.start(self.col, self.pm.sync_key(), None)
def _on_media_sync_start_stop(self):
self.toolbar.set_sync_active(self.media_syncer.is_syncing())
def _show_sync_log(self):
aqt.dialogs.open("sync_log", self, self.media_syncer)
# Tools
##########################################################################

View File

@ -7,14 +7,14 @@ import time
from concurrent.futures import Future
from copy import copy
from dataclasses import dataclass
from typing import Callable, List, Optional, Union
from typing import List, Optional, Union
import anki
import aqt
from anki import hooks
from anki.lang import _
from anki.media import media_paths_from_col_path
from anki.rsbackend import (
AnkiWebAuthFailed,
Interrupted,
MediaSyncDownloadedChanges,
MediaSyncDownloadedFiles,
@ -28,7 +28,7 @@ from anki.types import assert_impossible
from anki.utils import intTime
from aqt import gui_hooks
from aqt.qt import QDialog, QDialogButtonBox, QPushButton
from aqt.taskman import TaskManager
from aqt.utils import showInfo
@dataclass
@ -40,30 +40,24 @@ class MediaSyncState:
removed_files: int = 0
# fixme: make sure we don't run twice
# fixme: handle auth errors
# fixme: handle network errors
# fixme: show progress in UI
# fixme: abort when closing collection/app
# fixme: handle no hkey
# fixme: shards
# fixme: dialog should be a singleton
# fixme: abort button should not be default
# fixme: concurrent modifications during upload step
# fixme: mediaSanity
# fixme: corruptMediaDB
# fixme: autosync
# elif evt == "mediaSanity":
# showWarning(
# _(
# """\
# A problem occurred while syncing media. Please use Tools>Check Media, then \
# sync again to correct the issue."""
# )
# )
class SyncBegun:
pass
class SyncEnded:
pass
class SyncAborted:
pass
LogEntry = Union[MediaSyncState, SyncBegun, SyncEnded, SyncAborted]
LogEntry = Union[MediaSyncState, str]
@dataclass
@ -73,12 +67,11 @@ class LogEntryWithTime:
class MediaSyncer:
def __init__(self, taskman: TaskManager, on_start_stop: Callable[[], None]):
self._taskman = taskman
def __init__(self, mw: aqt.main.AnkiQt):
self.mw = mw
self._sync_state: Optional[MediaSyncState] = None
self._log: List[LogEntryWithTime] = []
self._want_stop = False
self._on_start_stop = on_start_stop
hooks.rust_progress_callback.append(self._on_rust_progress)
def _on_rust_progress(self, proceed: bool, progress: Progress) -> bool:
@ -104,14 +97,22 @@ class MediaSyncer:
elif isinstance(progress, MediaSyncRemovedFiles):
self._sync_state.removed_files += progress.files
def start(
self, col: anki.storage._Collection, hkey: str, shard: Optional[int]
) -> None:
def start(self) -> None:
"Start media syncing in the background, if it's not already running."
if self._sync_state is not None:
return
self._log_and_notify(SyncBegun())
hkey = self.mw.pm.sync_key()
if hkey is None:
return
if not self.mw.pm.media_syncing_enabled():
self._log_and_notify(_("Media syncing disabled."))
return
shard = None
self._log_and_notify(_("Media sync starting..."))
self._sync_state = MediaSyncState()
self._want_stop = False
self._on_start_stop()
@ -122,17 +123,17 @@ class MediaSyncer:
shard_str = ""
endpoint = f"https://sync{shard_str}ankiweb.net"
(media_folder, media_db) = media_paths_from_col_path(col.path)
(media_folder, media_db) = media_paths_from_col_path(self.mw.col.path)
def run() -> None:
col.backend.sync_media(hkey, media_folder, media_db, endpoint)
self.mw.col.backend.sync_media(hkey, media_folder, media_db, endpoint)
self._taskman.run_in_background(run, self._on_finished)
self.mw.taskman.run_in_background(run, self._on_finished)
def _log_and_notify(self, entry: LogEntry) -> None:
entry_with_time = LogEntryWithTime(time=intTime(), entry=entry)
self._log.append(entry_with_time)
self._taskman.run_on_main(
self.mw.taskman.run_on_main(
lambda: gui_hooks.media_sync_did_progress(entry_with_time)
)
@ -142,22 +143,38 @@ class MediaSyncer:
exc = future.exception()
if exc is not None:
if isinstance(exc, Interrupted):
self._log_and_notify(SyncAborted())
self._handle_sync_error(exc)
else:
self._log_and_notify(_("Media sync complete."))
def _handle_sync_error(self, exc: BaseException):
if isinstance(exc, AnkiWebAuthFailed):
self.mw.pm.set_sync_key(None)
self._log_and_notify(_("Authentication failed."))
showInfo(_("AnkiWeb ID or password was incorrect; please try again."))
elif isinstance(exc, Interrupted):
self._log_and_notify(_("Media sync aborted."))
else:
raise exc
else:
self._log_and_notify(SyncEnded())
def entries(self) -> List[LogEntryWithTime]:
return self._log
def abort(self) -> None:
if not self.is_syncing():
return
self._log_and_notify(_("Media sync aborting..."))
self._want_stop = True
def is_syncing(self) -> bool:
return self._sync_state is not None
def _on_start_stop(self):
self.mw.toolbar.set_sync_active(self.is_syncing())
def show_sync_log(self):
aqt.dialogs.open("sync_log", self.mw, self)
class MediaSyncDialog(QDialog):
silentlyClose = True
@ -170,6 +187,7 @@ class MediaSyncDialog(QDialog):
self.form.setupUi(self)
self.abort_button = QPushButton(_("Abort"))
self.abort_button.clicked.connect(self._on_abort) # type: ignore
self.abort_button.setAutoDefault(False)
self.form.buttonBox.addButton(self.abort_button, QDialogButtonBox.ActionRole)
gui_hooks.media_sync_did_progress.append(self._on_log_entry)
@ -191,9 +209,6 @@ class MediaSyncDialog(QDialog):
self.show()
def _on_abort(self, *args) -> None:
self.form.plainTextEdit.appendPlainText(
self._time_and_text(intTime(), _("Aborting..."))
)
self._syncer.abort()
self.abort_button.setHidden(True)
@ -202,12 +217,8 @@ class MediaSyncDialog(QDialog):
return f"{asctime}: {text}"
def _entry_to_text(self, entry: LogEntryWithTime):
if isinstance(entry.entry, SyncBegun):
txt = _("Media sync starting...")
elif isinstance(entry.entry, SyncEnded):
txt = _("Media sync complete.")
elif isinstance(entry.entry, SyncAborted):
txt = _("Aborted.")
if isinstance(entry.entry, str):
txt = entry.entry
elif isinstance(entry.entry, MediaSyncState):
txt = self._logentry_to_text(entry.entry)
else:
@ -227,3 +238,5 @@ class MediaSyncDialog(QDialog):
def _on_log_entry(self, entry: LogEntryWithTime):
self.form.plainTextEdit.appendPlainText(self._entry_to_text(entry))
if not self._syncer.is_syncing():
self.abort_button.setHidden(True)

View File

@ -515,6 +515,12 @@ please see:
def sync_key(self) -> Optional[str]:
return self.profile.get("syncKey")
def set_sync_key(self, val: Optional[str]) -> None:
self.profile["syncKey"] = val
def media_syncing_enabled(self) -> bool:
return self.profile["syncMedia"]
######################################################################
def apply_profile_options(self) -> None: