# Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # Profile handling ########################################################################## # - Saves in pickles rather than json to easily store Qt window state. # - Saves in sqlite rather than a flat file so the config can't be corrupted from aqt.qt import * import os, sys, time, random, cPickle, shutil, locale, re, atexit from anki.db import DB from anki.utils import isMac, isWin, intTime, checksum from anki.lang import langs, _ from aqt.utils import showWarning import aqt.forms metaConf = dict( ver=0, updates=True, created=intTime(), id=random.randrange(0, 2**63), lastMsg=-1, suppressUpdate=False, firstRun=True, defaultLang=None, ) profileConf = dict( # profile key=None, mainWindowGeom=None, mainWindowState=None, numBackups=30, lang="en", # editing fullSearch=False, searchHistory=[], recentColours=["#000000", "#0000ff"], stripHTML=True, editFontFamily='Arial', editFontSize=12, editLineSize=20, deleteMedia=False, preserveKeyboard=True, # reviewing autoplay=True, showDueTimes=True, showProgress=True, # syncing syncKey=None, syncMedia=True, proxyHost='', proxyPass='', proxyPort=8080, proxyUser='', ) class ProfileManager(object): def __init__(self, base=None, profile=None): self.name = None # instantiate base folder if not base: base = self._defaultBase() self.ensureBaseExists(base) self.checkPid(base) self.base = base # load database and cmdline-provided profile self._load() if profile: try: self.load(profile) except TypeError: raise Exception("Provided profile does not exist.") # Startup checks ###################################################################### # These routines run before the language code is initialized, so they # can't be translated def ensureBaseExists(self, base): if not os.path.exists(base): try: os.makedirs(base) except: QMessageBox.critical( None, "Error", """\ Anki can't write to the harddisk. Please see the \ documentation for information on using a flash drive.""") raise def checkPid(self, base): p = os.path.join(base, "pid") # check if an existing instance is running if os.path.exists(p): pid = int(open(p).read()) exists = False try: os.kill(pid, 0) exists = True except OSError: pass if exists: QMessageBox.warning( None, "Error", """\ Anki is already running. Please close the existing copy or restart your \ computer.""") raise Exception("Already running") # write out pid to the file open(p, "w").write(str(os.getpid())) # add handler to cleanup on exit def cleanup(): os.unlink(p) atexit.register(cleanup) # Profile load/save ###################################################################### def profiles(self): return sorted( x for x in self.db.list("select name from profiles") if x != "_global") def load(self, name, passwd=None): prof = cPickle.loads( self.db.scalar("select data from profiles where name = ?", name)) if prof['key'] and prof['key'] != self._pwhash(passwd): self.name = None raise Exception("Invalid password") if name != "_global": self.name = name self.profile = prof def save(self): sql = "update profiles set data = ? where name = ?" self.db.execute(sql, cPickle.dumps(self.profile), self.name) self.db.execute(sql, cPickle.dumps(self.meta), "_global") self.db.commit() def create(self, name): prof = profileConf.copy() prof['lang'] = self.meta['defaultLang'] self.db.execute("insert into profiles values (?, ?)", name, cPickle.dumps(prof)) self.db.commit() def remove(self, name): shutil.rmtree(self.profileFolder()) self.db.execute("delete from profiles where name = ?", name) self.db.commit() # Folder handling ###################################################################### def profileFolder(self): return self._ensureExists(os.path.join(self.base, self.name)) def addonFolder(self): return self._ensureExists(os.path.join(self.base, "addons")) def backupFolder(self): return self._ensureExists( os.path.join(self.profileFolder(), "backups")) def collectionPath(self): return os.path.join(self.profileFolder(), "collection.anki2") # Helpers ###################################################################### def _ensureExists(self, path): if not os.path.exists(path): os.makedirs(path) return path def _defaultBase(self): if isWin: s = QSettings(QSettings.UserScope, "Microsoft", "Windows") s.beginGroup("CurrentVersion/Explorer/Shell Folders") d = s.value("Personal") return os.path.join(d, "Anki") elif isMac: return os.path.expanduser("~/Documents/Anki") else: return os.path.expanduser("~/Anki") def _load(self): path = os.path.join(self.base, "prefs.db") new = not os.path.exists(path) self.db = DB(path, text=str) self.db.execute(""" create table if not exists profiles (name text primary key, data text not null);""") if new: # create a default global profile self.meta = metaConf.copy() self.db.execute("insert into profiles values ('_global', ?)", cPickle.dumps(metaConf)) self._setDefaultLang() # and save a default user profile for later (commits) self.create("User 1") else: # load previously created self.meta = cPickle.loads( self.db.scalar( "select data from profiles where name = '_global'")) def _pwhash(self, passwd): return checksum(unicode(self.meta['id'])+unicode(passwd)) # Default language ###################################################################### # On first run, allow the user to choose the default language def _setDefaultLang(self): # the dialog expects _ to be defined, but we're running before # setLang() has been called. so we create a dummy op for now import __builtin__ __builtin__.__dict__['_'] = lambda x: x # create dialog class NoCloseDiag(QDialog): def reject(self): pass d = self.langDiag = NoCloseDiag() f = self.langForm = aqt.forms.setlang.Ui_Dialog() f.setupUi(d) d.connect(d, SIGNAL("accepted()"), self._onLangSelected) d.connect(d, SIGNAL("rejected()"), lambda: True) # default to the system language (lang, enc) = locale.getdefaultlocale() if lang and lang not in ("pt_BR", "zh_CN", "zh_TW"): lang = re.sub("(.*)_.*", "\\1", lang) # find index idx = None en = None for c, (name, code) in enumerate(langs): if code == "en": en = c if code == lang: idx = c # if the system language isn't available, revert to english if idx is None: idx = en # update list f.lang.addItems([x[0] for x in langs]) f.lang.setCurrentRow(idx) d.exec_() def _onLangSelected(self): f = self.langForm code = langs[f.lang.currentRow()][1] self.meta['defaultLang'] = code sql = "update profiles set data = ? where name = ?" self.db.execute(sql, cPickle.dumps(self.meta), "_global") self.db.commit()