diff --git a/anki/media.py b/anki/media.py index 7493964b1..00971b729 100644 --- a/anki/media.py +++ b/anki/media.py @@ -221,14 +221,17 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); txt = re.sub(reg, "", txt) return txt - def escapeImages(self, string): + def escapeImages(self, string, unescape=False): def repl(match): tag = match.group(0) fname = match.group("fname") if re.match("(https?|ftp)://", fname): return tag - return tag.replace( - fname, urllib.quote(fname.encode("utf-8"))) + if unescape: + txt = urllib.unquote(fname) + else: + txt = urllib.quote(fname.encode("utf-8")) + return tag.replace(fname, txt) for reg in self.imgRegexps: string = re.sub(reg, repl, string) return string @@ -381,9 +384,13 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); if self.hasIllegal(f): continue # empty files are invalid; clean them up and continue - if not os.path.getsize(f): + sz = os.path.getsize(f) + if not sz: os.unlink(f) continue + if sz > 100*1024*1024: + self.col.log("ignoring file over 100MB", f) + continue # check encoding if not isMac: normf = unicodedata.normalize("NFC", f) @@ -439,6 +446,10 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); return self.db.scalar( "select count() from media where csum is not null") + def dirtyCount(self): + return self.db.scalar( + "select count() from media where dirty=1") + def forceResync(self): self.db.execute("delete from media") self.db.execute("update meta set lastUsn=0,dirMod=0") diff --git a/anki/sync.py b/anki/sync.py index cab986cae..2b9dc3b26 100644 --- a/anki/sync.py +++ b/anki/sync.py @@ -14,6 +14,7 @@ from anki.utils import ids2str, intTime, json, isWin, isMac, platDesc, checksum from anki.consts import * from hooks import runHook import anki +from lang import ngettext # syncing vars HTTP_TIMEOUT = 90 @@ -740,6 +741,7 @@ class MediaSyncer(object): # loop through and process changes from server self.col.log("last local usn is %s"%lastUsn) + self.downloadCount = 0 while True: data = self.server.mediaChanges(lastUsn=lastUsn) @@ -789,11 +791,16 @@ class MediaSyncer(object): # and we need to send our own updateConflict = False + toSend = self.col.media.dirtyCount() while True: zip, fnames = self.col.media.mediaChangesZip() if not fnames: break + runHook("syncMsg", ngettext( + "%d media change to upload", "%d media changes to upload", toSend) + % toSend) + processedCnt, serverLastUsn = self.server.uploadChanges(zip) self.col.media.markClean(fnames[0:processedCnt]) @@ -811,6 +818,8 @@ class MediaSyncer(object): self.col.media.db.commit() updateConflict = True + toSend -= processedCnt + if updateConflict: self.col.log("restart sync due to concurrent update") return self.sync() @@ -830,15 +839,14 @@ class MediaSyncer(object): self.col.log("fetch %s"%top) zipData = self.server.downloadFiles(files=top) cnt = self.col.media.addFilesFromZip(zipData) + self.downloadCount += cnt self.col.log("received %d files"%cnt) fnames = fnames[cnt:] - def files(self): - return self.col.media.addFilesToZip() - - def addFiles(self, zip): - "True if zip is the last in set. Server returns new usn instead." - return self.col.media.addFilesFromZip(zip) + n = self.downloadCount + runHook("syncMsg", ngettext( + "%d media file downloaded", "%d media files downloaded", n) + % n) # Remote media syncing ########################################################################## diff --git a/aqt/editor.py b/aqt/editor.py index 9b102b88c..b83c74678 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -449,6 +449,8 @@ class Editor(object): txt = self.mungeHTML(txt) # misbehaving apps may include a null byte in the text txt = txt.replace("\x00", "") + # reverse the url quoting we added to get images to display + txt = self.mw.col.media.escapeImages(txt, unescape=True) self.note.fields[self.currentField] = txt if not self.addMode: self.note.flush() diff --git a/aqt/errors.py b/aqt/errors.py index 533b37f46..1ce1421e6 100644 --- a/aqt/errors.py +++ b/aqt/errors.py @@ -65,6 +65,23 @@ Anki manual for more information.""") "other programs are not using the audio device.")) if "invalidTempFolder" in error: return showWarning(self.tempFolderMsg()) + if "disk I/O error" in error: + return showWarning(_("""\ +An error occurred while accessing the database. + +Possible causes: + +- Antivirus, firewall, backup, or synchronization software may be \ + interfering with Anki. Try disabling such software and see if the \ + problem goes away. +- Your disk may be full. +- The Documents/Anki folder may be on a network drive. +- Files in the Documents/Anki folder may not be writeable. +- Your hard disk may have errors. + +It's a good idea to run Tools>Check Database to ensure your collection \ +is not corrupt. +""")) stdText = _("""\ An error occurred. It may have been caused by a harmless bug,
or your deck may have a problem. diff --git a/aqt/sync.py b/aqt/sync.py index f56482266..e3bc5d517 100644 --- a/aqt/sync.py +++ b/aqt/sync.py @@ -116,6 +116,9 @@ 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")% @@ -296,6 +299,8 @@ class SyncThread(QThread): self.byteUpdate = time.time() def syncEvent(type): self.fireEvent("sync", type) + def syncMsg(msg): + self.fireEvent("syncMsg", msg) def canPost(): if (time.time() - self.byteUpdate) > 0.1: self.byteUpdate = time.time() @@ -309,6 +314,7 @@ class SyncThread(QThread): if canPost(): self.fireEvent("recv", self.recvTotal) addHook("sync", syncEvent) + addHook("syncMsg", syncMsg) addHook("httpSend", sendEvent) addHook("httpRecv", recvEvent) # run sync and catch any errors @@ -323,6 +329,7 @@ class SyncThread(QThread): # don't bump mod time unless we explicitly save self.col.close(save=False) remHook("sync", syncEvent) + remHook("syncMsg", syncMsg) remHook("httpSend", sendEvent) remHook("httpRecv", recvEvent) diff --git a/tests/test_media.py b/tests/test_media.py index 243c21516..b18969b5f 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -71,9 +71,11 @@ def test_changes(): d = getEmptyCol() assert d.media._changed() def added(): - return d.media.db.execute("select fname from log where type = 0") + return d.media.db.execute("select fname from media where csum is not null") + def removed(): + return d.media.db.execute("select fname from media where csum is null") assert not list(added()) - assert not list(d.media.removed()) + assert not list(removed()) # add a file dir = tempfile.mkdtemp(prefix="anki") path = os.path.join(dir, u"foo.jpg") @@ -83,24 +85,24 @@ def test_changes(): # should have been logged d.media.findChanges() assert list(added()) - assert not list(d.media.removed()) + assert not list(removed()) # if we modify it, the cache won't notice time.sleep(1) open(path, "w").write("world") assert len(list(added())) == 1 - assert not list(d.media.removed()) + assert not list(removed()) # but if we add another file, it will time.sleep(1) open(path+"2", "w").write("yo") d.media.findChanges() assert len(list(added())) == 2 - assert not list(d.media.removed()) + assert not list(removed()) # deletions should get noticed too time.sleep(1) os.unlink(path+"2") d.media.findChanges() assert len(list(added())) == 1 - assert len(list(d.media.removed())) == 1 + assert len(list(removed())) == 1 def test_illegal(): d = getEmptyCol()