profile tweaks

- handle unpickling of anki 2.0 prefs
- copy the prefs on first load, as python2 is not capable of reading the
protocol 3 pickles we write for proper bytes support
- when there's an error unpickling, write a clean copy of the
preferences instead of forgetting all profiles and starting from scratch
This commit is contained in:
Damien Elmes 2017-08-28 18:35:24 +10:00
parent a96ddfc3fd
commit b0a62838b5

View file

@ -10,7 +10,7 @@ import os
import random import random
import pickle import pickle
import shutil import shutil
import io
import locale import locale
import re import re
@ -132,25 +132,47 @@ a flash drive.""" % self.base)
self.db.list("select name from profiles") self.db.list("select name from profiles")
if x != "_global") if x != "_global")
def _unpickle(self, data):
class Unpickler(pickle.Unpickler):
def find_class(self, module, name):
fn = super().find_class(module, name)
if module == "sip" and name == "_unpickle_type":
def wrapper(mod, obj, args):
if mod.startswith("PyQt4") and obj == "QByteArray":
# can't trust str objects from python 2
return QByteArray()
return fn(mod, obj, args)
return wrapper
else:
return fn
up = Unpickler(io.BytesIO(data), errors="ignore")
return up.load()
def _pickle(self, obj):
return pickle.dumps(obj, protocol=0)
def load(self, name): def load(self, name):
assert name != "_global"
data = self.db.scalar("select cast(data as blob) from profiles where name = ?", name) data = self.db.scalar("select cast(data as blob) from profiles where name = ?", name)
# some profiles created in python2 may not decode properly self.name = name
prof = pickle.loads(data, errors="ignore") try:
if name != "_global": self.profile = self._unpickle(data)
self.name = name except:
self.profile = prof print("resetting corrupt profile")
self.profile = profileConf.copy()
self.save()
return True return True
def save(self): def save(self):
sql = "update profiles set data = ? where name = ?" sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, pickle.dumps(self.profile), self.name) self.db.execute(sql, self._pickle(self.profile), self.name)
self.db.execute(sql, pickle.dumps(self.meta), "_global") self.db.execute(sql, self._pickle(self.meta), "_global")
self.db.commit() self.db.commit()
def create(self, name): def create(self, name):
prof = profileConf.copy() prof = profileConf.copy()
self.db.execute("insert into profiles values (?, ?)", self.db.execute("insert or ignore into profiles values (?, ?)",
name, pickle.dumps(prof)) name, self._pickle(prof))
self.db.commit() self.db.commit()
def remove(self, name): def remove(self, name):
@ -265,7 +287,11 @@ and no other programs are accessing your profile folders, then try again."""))
return os.path.join(dataDir, "Anki2") return os.path.join(dataDir, "Anki2")
def _loadMeta(self): def _loadMeta(self):
opath = os.path.join(self.base, "prefs.db")
path = os.path.join(self.base, "prefs21.db") path = os.path.join(self.base, "prefs21.db")
if os.path.exists(opath) and not os.path.exists(path):
shutil.copy(opath, path)
new = not os.path.exists(path) new = not os.path.exists(path)
def recover(): def recover():
# if we can't load profile, start with a new one # if we can't load profile, start with a new one
@ -274,10 +300,7 @@ and no other programs are accessing your profile folders, then try again."""))
self.db.close() self.db.close()
except: except:
pass pass
broken = path+".broken" os.unlink(path)
if os.path.exists(broken):
os.unlink(broken)
os.rename(path, broken)
QMessageBox.warning( QMessageBox.warning(
None, "Preferences Corrupt", """\ None, "Preferences Corrupt", """\
Anki's prefs21.db file was corrupt and has been recreated. If you were using multiple \ Anki's prefs21.db file was corrupt and has been recreated. If you were using multiple \
@ -287,23 +310,22 @@ profiles, please add them back using the same names to recover your cards.""")
self.db.execute(""" self.db.execute("""
create table if not exists profiles create table if not exists profiles
(name text primary key, data text not null);""") (name text primary key, data text not null);""")
data = self.db.scalar(
"select cast(data as blob) from profiles where name = '_global'")
except: except:
recover() recover()
return self._loadMeta() return self._loadMeta()
if not new: if not new:
# load previously created # load previously created data
try: try:
self.meta = pickle.loads( self.meta = self._unpickle(data)
self.db.scalar(
"select cast(data as blob) from profiles where name = '_global'"))
return return
except: except:
recover() print("resetting corrupt _global")
return self._loadMeta()
# create a default global profile # create a default global profile
self.meta = metaConf.copy() self.meta = metaConf.copy()
self.db.execute("insert or replace into profiles values ('_global', ?)", self.db.execute("insert or replace into profiles values ('_global', ?)",
pickle.dumps(metaConf)) self._pickle(metaConf))
self._setDefaultLang() self._setDefaultLang()
return True return True
@ -378,6 +400,6 @@ please see:
def setLang(self, code): def setLang(self, code):
self.meta['defaultLang'] = code self.meta['defaultLang'] = code
sql = "update profiles set data = ? where name = ?" sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, pickle.dumps(self.meta), "_global") self.db.execute(sql, self._pickle(self.meta), "_global")
self.db.commit() self.db.commit()
anki.lang.setLang(code, local=False) anki.lang.setLang(code, local=False)