Squashed commit of the following:

commit cb509e8f75e3dcdbc66327be4bfbf6661aa084b5
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 22:06:28 2013 +0100

    Cut down 'import' statements to only modules actually used.

commit 0ea255115e095e31af5a991e9cce2b5b15cb496d
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 22:00:06 2013 +0100

     * Add getCollectionManager() so that the whole process can share the same ThreadingCollectionManager object.

     * Got the RestApp actually working!

commit 00997bab600b13d4b430ed2c2839b1d2232f55ed
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 21:04:58 2013 +0100

    Got the sync_app working again (more or less)

commit 459c69566bb92d2c0195a384e067d98c059bdea7
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 19:47:40 2013 +0100

    Started implementing test for the RESTful callbacks that PrepECN is going to need.

commit 7ffbac793f9bf45ab9056c1de475422b8742e107
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 17:19:06 2013 +0100

    Started work on a WSGI app for RESTful access to Anki based on Bibliobird code here:

      https://raw.github.com/dsnopek/bbcom/master/AnkiServer/AnkiServer/deck.py

commit 8820411388ce0c2b7b14769c614c22c675d2dbdd
Author: David Snopek <dsnopek@gmail.com>
Date:   Fri Jul 12 15:03:56 2013 +0100

     * Seperated the collection and threading code.

     * Implemented a new interface to interact with the collections, which will hopefully be more transparent and testable.
This commit is contained in:
David Snopek
2013-07-12 22:08:16 +01:00
parent 661662400f
commit e25cf25684
12 changed files with 597 additions and 202 deletions

View File

@@ -0,0 +1,2 @@
# package

225
AnkiServer/apps/rest_app.py Normal file
View File

@@ -0,0 +1,225 @@
from webob.dec import wsgify
from webob.exc import *
from webob import Response
try:
import simplejson as json
except ImportError:
import json
import os, logging
__all__ = ['RestApp', 'RestHandlerBase', 'hasReturnValue', 'noReturnValue']
def hasReturnValue(func):
func.hasReturnValue = True
return func
def noReturnValue(func):
func.hasReturnValue = False
return func
class RestHandlerBase(object):
"""Parent class for single handler callbacks."""
hasReturnValue = True
def __call__(self, collection, data, ids):
pass
class RestHandlerGroupBase(object):
"""Parent class for a handler group."""
hasReturnValue = True
class _RestHandlerWrapper(RestHandlerBase):
def __init__(self, func_name, func, hasReturnValue=True):
self.func_name = func_name
self.func = func
self.hasReturnValue = hasReturnValue
def __call__(self, *args, **kw):
return self.func(*args, **kw)
class RestApp(object):
"""A WSGI app that implements RESTful operations on Collections, Decks and Cards."""
handler_types = ['collection', 'deck', 'note']
def __init__(self, data_root, allowed_hosts='*', use_default_handlers=True, collection_manager=None):
from AnkiServer.threading import getCollectionManager
self.data_root = os.path.abspath(data_root)
self.allowed_hosts = allowed_hosts
if collection_manager is not None:
self.collection_manager = collection_manager
else:
self.collection_manager = getCollectionManager()
self.handlers = {}
for type in self.handler_types:
self.handlers[type] = {}
if use_default_handlers:
self.add_handler_group('collection', CollectionHandlerGroup())
self.add_handler_group('deck', DeckHandlerGroup())
self.add_handler_group('note', NoteHandlerGroup())
def _get_path(self, path):
npath = os.path.normpath(os.path.join(self.data_root, path, 'collection.anki2'))
if npath[0:len(self.data_root)] != self.data_root:
# attempting to escape our data jail!
raise HTTPBadRequest('"%s" is not a valid path/id' % path)
return npath
def add_handler(self, type, name, handler):
"""Adds a callback handler for a type (collection, deck, card) with a unique name.
- 'type' is the item that will be worked on, for example: collection, deck, and card.
- 'name' is a unique name for the handler that gets used in the URL.
- 'handler' handler can be a Python method or a subclass of the RestHandlerBase class.
"""
if self.handlers[type].has_key(name):
raise "Handler already for %(type)s/%(name)s exists!"
self.handlers[type][name] = handler
def add_handler_group(self, type, group):
"""Adds several handlers for every public method on an object descended from RestHandlerGroup.
This allows you to create a single class with several methods, so that you can quickly
create a group of related handlers."""
import inspect
for name, method in inspect.getmembers(group, predicate=inspect.ismethod):
if not name.startswith('_'):
if hasattr(group, 'hasReturnValue') and not hasattr(method, 'hasReturnValue'):
method = _RestHandlerWrapper(group.__class__.__name__ + '.' + name, method, group.hasReturnValue)
self.add_handler(type, name, method)
@wsgify
def __call__(self, req):
if self.allowed_hosts != '*':
try:
remote_addr = req.headers['X-Forwarded-For']
except KeyError:
remote_addr = req.remote_addr
if remote_addr != self.allowed_hosts:
raise HTTPForbidden()
if req.method != 'POST':
raise HTTPMethodNotAllowed(allow=['POST'])
# split the URL into a list of parts
path = req.path
if path[0] == '/':
path = path[1:]
parts = path.split('/')
# pull the type and context from the URL parts
type = None
ids = []
for type in self.handler_types:
if len(parts) == 0 or parts.pop(0) != type:
break
if len(parts) > 0:
ids.append(parts.pop(0))
if len(parts) < 2:
break
# sanity check to make sure the URL is valid
if type is None or len(parts) > 1 or len(ids) == 0:
raise HTTPNotFound()
# get the handler name
if len(parts) == 0:
name = 'index'
else:
name = parts[0]
# get the collection path
collection_path = self._get_path(ids[0])
print collection_path
# get the handler function
try:
handler = self.handlers[type][name]
except KeyError:
raise HTTPNotFound()
# get if we have a return value
hasReturnValue = True
if hasattr(handler, 'hasReturnValue'):
hasReturnValue = handler.hasReturnValue
try:
data = json.loads(req.body)
except ValueError, e:
logging.error(req.path+': Unable to parse JSON: '+str(e), exc_info=True)
raise HTTPBadRequest()
# make the keys into non-unicode strings
data = dict([(str(k), v) for k, v in data.items()])
# debug
from pprint import pprint
pprint(data)
# run it!
col = self.collection_manager.get_collection(collection_path)
try:
output = col.execute(handler, [data, ids], {}, hasReturnValue)
except Exception, e:
logging.error(e)
return HTTPInternalServerError()
if output is None:
return Response('', content_type='text/plain')
else:
return Response(json.dumps(output), content_type='application/json')
class CollectionHandlerGroup(RestHandlerGroupBase):
"""Default handler group for 'collection' type."""
def list_decks(self, col, data, ids):
return col.decks.all()
@noReturnValue
def select_deck(self, col, data, ids):
col.decks.select(data['deck_id'])
class DeckHandlerGroup(RestHandlerGroupBase):
"""Default handler group for 'deck' type."""
def next_card(self, col, data, ids):
deck_id = ids[1]
col.decks.select(deck_id)
card = col.sched.getCard()
return card
class NoteHandlerGroup(RestHandlerGroupBase):
"""Default handler group for 'note' type."""
def add_new(self, col, data, ids):
# col.addNote(...)
pass
# Our entry point
def make_app(global_conf, **local_conf):
# setup the logger
logging_config_file = local_conf.get('logging.config_file')
if logging_config_file:
# monkey patch the logging.config.SMTPHandler if necessary
import sys
if sys.version_info[0] == 2 and sys.version_info[1] == 5:
import AnkiServer.logpatch
# load the config file
import logging.config
logging.config.fileConfig(logging_config_file)
return RestApp(
data_root=local_conf.get('data_root', '.'),
allowed_hosts=local_conf.get('allowed_hosts', '*')
)

315
AnkiServer/apps/sync_app.py Normal file
View File

@@ -0,0 +1,315 @@
from webob.dec import wsgify
from webob.exc import *
from webob import Response
import AnkiServer
import anki
from anki.sync import LocalServer, MediaSyncer
try:
import simplejson as json
except ImportError:
import json
import os
class SyncCollectionHandler(LocalServer):
operations = ['meta', 'applyChanges', 'start', 'chunk', 'applyChunk', 'sanityCheck2', 'finish']
def __init__(self, col):
LocalServer.__init__(self, col)
def applyChanges(self, changes):
#self.lmod, lscm, self.maxUsn, lts, dummy = self.meta()
# TODO: how should we set this value?
#self.lnewer = 1
result = LocalServer.applyChanges(self, changes)
#self.prepareToChunk()
return result
#def chunk(self, ):
# self.prepareToChunk()
# return LocalServer.chunk()
class SyncMediaHandler(MediaSyncer):
operations = ['remove', 'files', 'addFiles', 'mediaSanity']
def __init__(self, col):
MediaSyncer.__init__(self, col)
def files(self, minUsn=0):
import zipfile, StringIO
zipdata, fnames = MediaSyncer.files(self)
# add a _usn element to the zipdata
fd = StringIO.StringIO(zipdata)
zfd = zipfile.ZipFile(fd, "a", compression=zipfile.ZIP_DEFLATED)
zfd.writestr("_usn", str(minUsn + len(fnames)))
zfd.close()
return fd.getvalue()
class SyncUserSession(object):
def __init__(self, name, path, collection_manager):
import time
self.name = name
self.path = path
self.collection_manager = collection_manager
self.version = 0
self.created = time.time()
# make sure the user path exists
if not os.path.exists(path):
os.mkdir(path)
self.collection_handler = None
self.media_handler = None
def get_collection_path(self):
return os.path.realpath(os.path.join(self.path, 'collection.anki2'))
def get_thread(self):
return self.collection_manager.get_collection(self.get_collection_path())
def get_handler_for_operation(self, operation, col):
if operation in SyncCollectionHandler.operations:
cache_name, handler_class = 'collection_handler', SyncCollectionHandler
else:
cache_name, handler_class = 'media_handler', SyncMediaHandler
if getattr(self, cache_name) is None:
setattr(self, cache_name, handler_class(col))
return getattr(self, cache_name)
class SyncApp(object):
valid_urls = SyncCollectionHandler.operations + SyncMediaHandler.operations + ['hostKey', 'upload', 'download', 'getDecks']
def __init__(self, **kw):
from AnkiServer.threading import getCollectionManager
self.data_root = os.path.abspath(kw.get('data_root', '.'))
self.base_url = kw.get('base_url', '/')
self.sessions = {}
try:
self.collection_manager = kw['collection_manager']
except KeyError:
self.collection_manager = getCollectionManager()
# make sure the base_url has a trailing slash
if len(self.base_url) == 0:
self.base_url = '/'
elif self.base_url[-1] != '/':
self.base_url = base_url + '/'
def authenticate(self, username, password):
"""
Returns True if this username is allowed to connect with this password. False otherwise.
Override this to change how users are authenticated.
"""
# TODO: This should have the exact opposite default ;-)
return True
def username2dirname(self, username):
"""
Returns the directory name for the given user. By default, this is just the username.
Override this to adjust the mapping between users and their directory.
"""
return username
def generateHostKey(self, username):
"""Generates a new host key to be used by the given username to identify their session.
This values is random."""
import hashlib, time, random, string
chars = string.ascii_letters + string.digits
val = ':'.join([username, str(int(time.time())), ''.join(random.choice(chars) for x in range(8))])
return hashlib.md5(val).hexdigest()
def create_session(self, hkey, username, user_path):
"""Creates, stores and returns a new session for the given hkey and username."""
session = self.sessions[hkey] = SyncUserSession(username, user_path, self.collection_manager)
return session
def load_session(self, hkey):
return self.sessions.get(hkey)
def save_session(self, hkey, session):
pass
def delete_session(self, hkey):
del self.sessions[hkey]
def _decode_data(self, data, compression=0):
import gzip, StringIO
if compression:
buf = gzip.GzipFile(mode="rb", fileobj=StringIO.StringIO(data))
data = buf.read()
buf.close()
# really lame check for JSON
if data[0] == '{' and data[-1] == '}':
data = json.loads(data)
else:
data = {'data': data}
return data
def operation_upload(self, col, data, session):
# TODO: deal with thread pool
fd = open(session.get_collection_path(), 'wb')
fd.write(data)
fd.close()
def operation_download(self, col, data, session):
pass
@wsgify
def __call__(self, req):
print req.path
if req.path.startswith(self.base_url):
url = req.path[len(self.base_url):]
if url not in self.valid_urls:
raise HTTPNotFound()
if url == 'getDecks':
# This is an Anki 1.x client! Tell them to upgrade.
import zlib
return Response(
status='200 OK',
content_type='application/json',
content_encoding='deflate',
body=zlib.compress(json.dumps({'status': 'oldVersion'})))
try:
compression = req.POST['c']
except KeyError:
compression = 0
try:
data = req.POST['data'].file.read()
data = self._decode_data(data, compression)
except KeyError:
data = {}
except ValueError:
# Bad JSON
raise HTTPBadRequest()
print 'data:', data
if url == 'hostKey':
try:
u = data['u']
p = data['p']
except KeyError:
raise HTTPForbidden('Must pass username and password')
if self.authenticate(u, p):
dirname = self.username2dirname(u)
if dirname is None:
raise HTTPForbidden()
hkey = self.generateHostKey(u)
user_path = os.path.join(self.data_root, dirname)
session = self.create_session(hkey, u, user_path)
result = {'key': hkey}
return Response(
status='200 OK',
content_type='application/json',
body=json.dumps(result))
else:
# TODO: do I have to pass 'null' for the client to receive None?
raise HTTPForbidden('null')
# Get and verify the session
try:
hkey = req.POST['k']
except KeyError:
raise HTTPForbidden()
session = self.load_session(hkey)
if session is None:
raise HTTPForbidden()
if url in SyncCollectionHandler.operations + SyncMediaHandler.operations:
# 'meta' passes the SYNC_VER but it isn't used in the handler
if url == 'meta' and data.has_key('v'):
session.version = data['v']
del data['v']
# Create a closure to run this operation inside of the thread allocated to this collection
def runFunc(col):
handler = session.get_handler_for_operation(url, col)
func = getattr(handler, url)
result = func(**data)
handler.col.save()
return result
runFunc.func_name = url
# Send to the thread to execute
thread = session.get_thread()
result = thread.execute(runFunc)
# If it's a complex data type, we convert it to JSON
if type(result) not in (str, unicode):
result = json.dumps(result)
if url == 'finish':
self.delete_session(hkey)
return Response(
status='200 OK',
content_type='application/json',
body=result)
elif url in ('upload', 'download'):
if url == 'upload':
func = self.operation_upload
else:
func = self.operation_download
thread = session.get_thread()
thread.execute(self.operation_upload, [data['data'], session])
return Response(
status='200 OK',
content_type='text/plain',
body='OK')
# This was one of our operations but it didn't get handled... Oops!
raise HTTPInternalServerError()
return Response(status='200 OK', content_type='text/plain', body='Anki Sync Server')
# Our entry point
def make_app(global_conf, **local_conf):
return SyncApp(**local_conf)
def main():
from wsgiref.simple_server import make_server
from AnkiServer.threading import shutdown
ankiserver = SyncApp()
httpd = make_server('', 8001, ankiserver)
try:
print "Starting..."
httpd.serve_forever()
except KeyboardInterrupt:
print "Exiting ..."
finally:
shutdown()
if __name__ == '__main__': main()

377
AnkiServer/apps/sync_old.py Normal file
View File

@@ -0,0 +1,377 @@
from webob.dec import wsgify
from webob.exc import *
from webob import Response
import anki
from anki.sync import HttpSyncServer, CHUNK_SIZE
from anki.db import sqlite
from anki.utils import checksum
import AnkiServer.deck
import MySQLdb
try:
import simplejson as json
except ImportError:
import json
import os, zlib, tempfile, time
def makeArgs(mdict):
d = dict(mdict.items())
# TODO: use password/username/version for something?
for k in ['p','u','v','d']:
if d.has_key(k):
del d[k]
return d
class FileIterable(object):
def __init__(self, fn):
self.fn = fn
def __iter__(self):
return FileIterator(self.fn)
class FileIterator(object):
def __init__(self, fn):
self.fn = fn
self.fo = open(self.fn, 'rb')
self.c = zlib.compressobj()
self.flushed = False
def __iter__(self):
return self
def next(self):
data = self.fo.read(CHUNK_SIZE)
if not data:
if not self.flushed:
self.flushed = True
return self.c.flush()
else:
raise StopIteration
return self.c.compress(data)
def lock_deck(path):
""" Gets exclusive access to this deck path. If there is a DeckThread running on this
deck, this will wait for its current operations to complete before temporarily stopping
it. """
from AnkiServer.deck import thread_pool
if thread_pool.decks.has_key(path):
thread_pool.decks[path].stop_and_wait()
thread_pool.lock(path)
def unlock_deck(path):
""" Release exclusive access to this deck path. """
from AnkiServer.deck import thread_pool
thread_pool.unlock(path)
class SyncAppHandler(HttpSyncServer):
operations = ['summary','applyPayload','finish','createDeck','getOneWayPayload']
def __init__(self):
HttpSyncServer.__init__(self)
def createDeck(self, name):
# The HttpSyncServer.createDeck doesn't return a valid value! This seems to be
# a bug in libanki.sync ...
return self.stuff({"status": "OK"})
def finish(self):
# The HttpSyncServer has no finish() function... I can only assume this is a bug too!
return self.stuff("OK")
class SyncApp(object):
valid_urls = SyncAppHandler.operations + ['getDecks','fullup','fulldown']
def __init__(self, **kw):
self.data_root = os.path.abspath(kw.get('data_root', '.'))
self.base_url = kw.get('base_url', '/')
self.users = {}
# make sure the base_url has a trailing slash
if len(self.base_url) == 0:
self.base_url = '/'
elif self.base_url[-1] != '/':
self.base_url = base_url + '/'
# setup mysql connection
mysql_args = {}
for k, v in kw.items():
if k.startswith('mysql.'):
mysql_args[k[6:]] = v
self.mysql_args = mysql_args
self.conn = None
# get SQL statements
self.sql_check_password = kw.get('sql_check_password')
self.sql_username2dirname = kw.get('sql_username2dirname')
default_libanki_version = '.'.join(anki.version.split('.')[:2])
def user_libanki_version(self, u):
try:
s = self.users[u]['libanki']
except KeyError:
return self.default_libanki_version
parts = s.split('.')
if parts[0] == '1':
if parts[1] == '0':
return '1.0'
elif parts[1] in ('1','2'):
return '1.2'
return self.default_libanki_version
# Mimcs from anki.sync.SyncTools.stuff()
def _stuff(self, data):
return zlib.compress(json.dumps(data))
def _connect_mysql(self):
if self.conn is None and len(self.mysql_args) > 0:
self.conn = MySQLdb.connect(**self.mysql_args)
def _execute_sql(self, sql, args=()):
self._connect_mysql()
try:
cur = self.conn.cursor()
cur.execute(sql, args)
except MySQLdb.OperationalError, e:
if e.args[0] == 2006:
# MySQL server has gone away message
self.conn = None
self._connect_mysql()
cur = self.conn.cursor()
cur.execute(sql, args)
return cur
def check_password(self, username, password):
if len(self.mysql_args) > 0 and self.sql_check_password is not None:
cur = self._execute_sql(self.sql_check_password, (username, password))
row = cur.fetchone()
return row is not None
return True
def username2dirname(self, username):
if len(self.mysql_args) > 0 and self.sql_username2dirname is not None:
cur = self._execute_sql(self.sql_username2dirname, (username,))
row = cur.fetchone()
if row is None:
return None
return str(row[0])
return username
def _getDecks(self, user_path):
decks = {}
if os.path.exists(user_path):
# It is a dict of {'deckName':[modified,lastSync]}
for fn in os.listdir(unicode(user_path, 'utf-8')):
if len(fn) > 5 and fn[-5:] == '.anki':
d = os.path.abspath(os.path.join(user_path, fn))
# For simplicity, we will always open a thread. But this probably
# isn't necessary!
thread = AnkiServer.deck.thread_pool.start(d)
def lookupModifiedLastSync(wrapper):
deck = wrapper.open()
return [deck.modified, deck.lastSync]
res = thread.execute(lookupModifiedLastSync, [thread.wrapper])
# if thread_pool.threads.has_key(d):
# thread = thread_pool.threads[d]
# def lookupModifiedLastSync(wrapper):
# deck = wrapper.open()
# return [deck.modified, deck.lastSync]
# res = thread.execute(lookup, [thread.wrapper])
# else:
# conn = sqlite.connect(d)
# cur = conn.cursor()
# cur.execute("select modified, lastSync from decks")
#
# res = list(cur.fetchone())
#
# cur.close()
# conn.close()
#self.decks[fn[:-5]] = ["%.5f" % x for x in res]
decks[fn[:-5]] = res
# same as HttpSyncServer.getDecks()
return self._stuff({
"status": "OK",
"decks": decks,
"timestamp": time.time(),
})
def _fullup(self, wrapper, infile, version):
wrapper.close()
path = wrapper.path
# DRS: most of this function was graciously copied
# from anki.sync.SyncTools.fullSyncFromServer()
(fd, tmpname) = tempfile.mkstemp(dir=os.getcwd(), prefix="fullsync")
outfile = open(tmpname, 'wb')
decomp = zlib.decompressobj()
while 1:
data = infile.read(CHUNK_SIZE)
if not data:
outfile.write(decomp.flush())
break
outfile.write(decomp.decompress(data))
infile.close()
outfile.close()
os.close(fd)
# if we were successful, overwrite old deck
if os.path.exists(path):
os.unlink(path)
os.rename(tmpname, path)
# reset the deck name
c = sqlite.connect(path)
lastSync = time.time()
if version == '1':
c.execute("update decks set lastSync = ?", [lastSync])
elif version == '2':
c.execute("update decks set syncName = ?, lastSync = ?",
[checksum(path.encode("utf-8")), lastSync])
c.commit()
c.close()
return lastSync
def _stuffedResp(self, data):
return Response(
status='200 OK',
content_type='application/json',
content_encoding='deflate',
body=data)
@wsgify
def __call__(self, req):
if req.path.startswith(self.base_url):
url = req.path[len(self.base_url):]
if url not in self.valid_urls:
raise HTTPNotFound()
# get and check username and password
try:
u = req.str_params.getone('u')
p = req.str_params.getone('p')
except KeyError:
raise HTTPBadRequest('Must pass username and password')
if not self.check_password(u, p):
#raise HTTPBadRequest('Incorrect username or password')
return self._stuffedResp(self._stuff({'status':'invalidUserPass'}))
dirname = self.username2dirname(u)
if dirname is None:
raise HTTPBadRequest('Incorrect username or password')
user_path = os.path.join(self.data_root, dirname)
# get and lock the (optional) deck for this request
d = None
try:
d = unicode(req.str_params.getone('d'), 'utf-8')
# AnkiDesktop actually passes us the string value 'None'!
if d == 'None':
d = None
except KeyError:
pass
if d is not None:
# get the full deck path name
d = os.path.abspath(os.path.join(user_path, d)+'.anki')
if d[:len(user_path)] != user_path:
raise HTTPBadRequest('Bad deck name')
thread = AnkiServer.deck.thread_pool.start(d)
else:
thread = None
if url == 'getDecks':
# force the version up to 1.2.x
v = req.str_params.getone('libanki')
if v.startswith('0.') or v.startswith('1.0'):
return self._stuffedResp(self._stuff({'status':'oldVersion'}))
# store the data the user passes us keyed with the username. This
# will be used later by SyncAppHandler for version compatibility.
self.users[u] = makeArgs(req.str_params)
return self._stuffedResp(self._getDecks(user_path))
elif url in SyncAppHandler.operations:
handler = SyncAppHandler()
func = getattr(handler, url)
args = makeArgs(req.str_params)
if thread is not None:
# If this is for a specific deck, then it needs to run
# inside of the DeckThread.
def runFunc(wrapper):
handler.deck = wrapper.open()
ret = func(**args)
handler.deck.save()
return ret
runFunc.func_name = url
ret = thread.execute(runFunc, [thread.wrapper])
else:
# Otherwise, we can simply execute it in this thread.
ret = func(**args)
# clean-up user data stored in getDecks
if url == 'finish':
del self.users[u]
return self._stuffedResp(ret)
elif url == 'fulldown':
# set the syncTime before we send it
def setupForSync(wrapper):
wrapper.close()
c = sqlite.connect(d)
lastSync = time.time()
c.execute("update decks set lastSync = ?", [lastSync])
c.commit()
c.close()
thread.execute(setupForSync, [thread.wrapper])
return Response(status='200 OK', content_type='application/octet-stream', content_encoding='deflate', content_disposition='attachment; filename="'+os.path.basename(d).encode('utf-8')+'"', app_iter=FileIterable(d))
elif url == 'fullup':
#version = self.user_libanki_version(u)
try:
version = req.str_params.getone('v')
except KeyError:
version = '1'
infile = req.str_params['deck'].file
lastSync = thread.execute(self._fullup, [thread.wrapper, infile, version])
# append the 'lastSync' value for libanki 1.1 and 1.2
if version == '2':
body = 'OK '+str(lastSync)
else:
body = 'OK'
return Response(status='200 OK', content_type='application/text', body=body)
return Response(status='200 OK', content_type='text/plain', body='Anki Server')
# Our entry point
def make_app(global_conf, **local_conf):
return SyncApp(**local_conf)
def main():
from wsgiref.simple_server import make_server
ankiserver = DeckApp('.', '/sync/')
httpd = make_server('', 8001, ankiserver)
try:
httpd.serve_forever()
except KeyboardInterrupt:
print "Exiting ..."
finally:
AnkiServer.deck.thread_pool.shutdown()
if __name__ == '__main__': main()