From 0cce540d83b7e8179be410db62487acef0472e1c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 29 Jan 2018 14:12:04 +1000 Subject: [PATCH] enforce schema version; allow importing+exporting v2 scheduler To avoid all sorts of problems, we need to ensure cards scheduled with the V2 scheduler are not studied in older clients. Unfortunately we can't just bump the file's schema version, as the existing clients will freely import files created with newer versions. This patch changes that, so things should be a bit easier in the future. In the mean time, we need a way to prevent older clients from importing files created with the V2 scheduler. To do this, we switch to using a 'collection.anki21' file in the archive, and include a dummy collection .anki2 file. The code has been tested with both deck and collection packages, but exporting deck packages w/ scheduling info will remain disabled until the V2 scheduler has had more testing. --- anki/exporting.py | 40 ++++++++++++++++++++++++++++++++++------ anki/importing/anki2.py | 3 +++ anki/importing/apkg.py | 11 +++++++++-- anki/storage.py | 2 ++ aqt/importing.py | 12 ++++++++++-- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/anki/exporting.py b/anki/exporting.py index 2ae5d40cd..3df18498e 100644 --- a/anki/exporting.py +++ b/anki/exporting.py @@ -4,7 +4,7 @@ import re, os, zipfile, shutil from anki.lang import _ -from anki.utils import ids2str, splitFields, json +from anki.utils import ids2str, splitFields, json, namedtmp from anki.hooks import runHook from anki import Collection @@ -113,6 +113,9 @@ class AnkiExporter(Exporter): Exporter.__init__(self, col) def exportInto(self, path): + # sched info+v2 scheduler not compatible w/ older clients + self._v2sched = self.col.schedVer() != 1 and self.includeSched + # create a new collection at the target try: os.unlink(path) @@ -263,13 +266,19 @@ class AnkiPackageExporter(AnkiExporter): z.close() def doExport(self, z, path): - if self.col.schedVer() != 1: - raise Exception("Experimental scheduler currently doesn't support deck exports.") - # export into the anki2 file colfile = path.replace(".apkg", ".anki2") AnkiExporter.exportInto(self, colfile) - z.write(colfile, "collection.anki2") + if not self._v2sched: + z.write(colfile, "collection.anki2") + else: + # fixme: remove in the future + raise Exception("Please switch to the normal scheduler before exporting a single deck with scheduling information.") + + # prevent older clients from accessing + self._addDummyCollection(z) + z.write(colfile, "collection.anki21") + # and media self.prepareMedia() media = self._exportMedia(z, self.mediaFiles, self.mediaDir) @@ -304,6 +313,20 @@ class AnkiPackageExporter(AnkiExporter): # is zipped up pass + # create a dummy collection to ensure older clients don't try to read + # data they don't understand + def _addDummyCollection(self, zip): + path = namedtmp("dummy.anki2") + c = Collection(path) + n = c.newNote() + n['Front'] = "This file requires a newer version of Anki." + c.addNote(n) + c.save() + c.close() + + zip.write(path, "collection.anki2") + os.unlink(path) + # Collection package ###################################################################### @@ -320,8 +343,13 @@ class AnkiCollectionPackageExporter(AnkiPackageExporter): def doExport(self, z, path): # close our deck & write it into the zip file, and reopen self.count = self.col.cardCount() + v2 = self.col.schedVer() != 1 self.col.close() - z.write(self.col.path, "collection.anki2") + if not v2: + z.write(self.col.path, "collection.anki2") + else: + self._addDummyCollection(z) + z.write(self.col.path, "collection.anki21") self.col.reopen() # copy all media if not self.includeMedia: diff --git a/anki/importing/anki2.py b/anki/importing/anki2.py index dfd4f1a0d..9510176ff 100644 --- a/anki/importing/anki2.py +++ b/anki/importing/anki2.py @@ -31,6 +31,9 @@ class Anki2Importer(Importer): self.src.close(save=False) def _prepareFiles(self): + if self.file.endswith(".anki21") and self.col.schedVer() == 1: + raise Exception("V2 scheduler must be enabled to import this file.") + self.dst = self.col self.src = Collection(self.file) diff --git a/anki/importing/apkg.py b/anki/importing/apkg.py index e31bc4b07..2e28e4400 100644 --- a/anki/importing/apkg.py +++ b/anki/importing/apkg.py @@ -12,8 +12,15 @@ class AnkiPackageImporter(Anki2Importer): def run(self): # extract the deck from the zip file self.zip = z = zipfile.ZipFile(self.file) - col = z.read("collection.anki2") - colpath = tmpfile(suffix=".anki2") + # v2 scheduler? + try: + z.getinfo("collection.anki21") + suffix = ".anki21" + except KeyError: + suffix = ".anki2" + + col = z.read("collection"+suffix) + colpath = tmpfile(suffix=suffix) with open(colpath, "wb") as f: f.write(col) self.file = colpath diff --git a/anki/storage.py b/anki/storage.py index b6ca539dc..afb382845 100644 --- a/anki/storage.py +++ b/anki/storage.py @@ -40,6 +40,8 @@ def Collection(path, lock=True, server=False, sync=True, log=False): col = _Collection(db, server, log) if ver < SCHEMA_VERSION: _upgrade(col, ver) + elif ver > SCHEMA_VERSION: + raise Exception("This file requires a newer version of Anki.") elif create: # add in reverse order so basic is default addClozeModel(col) diff --git a/aqt/importing.py b/aqt/importing.py index 499beb857..de45cc6be 100644 --- a/aqt/importing.py +++ b/aqt/importing.py @@ -388,10 +388,18 @@ def replaceWithApkg(mw, file, backup): def _replaceWithApkg(mw, file, backup): mw.progress.start(immediate=True) - # overwrite collection + z = zipfile.ZipFile(file) + + # v2 scheduler? + colname = "collection.anki21" try: - z.extract("collection.anki2", mw.pm.profileFolder()) + z.getinfo(colname) + except KeyError: + colname = "collection.anki2" + + try: + z.extract(colname, mw.pm.profileFolder()) except: mw.progress.finish() showWarning(_("The provided file is not a valid .apkg file."))