├── README.md └── koreader-flask.py /README.md: -------------------------------------------------------------------------------- 1 | # koreader-sync 2 | 3 | ## Description 4 | 5 | Quick and dirty implementation of the KOReader (https://github.com/koreader/koreader) sync service. 6 | I found stock implementation (https://github.com/koreader/koreader-sync-server) too heavy for my personal needs. 7 | 8 | ## Dependencies 9 | 10 | * Flask : http://flask.pocoo.org/ 11 | * pyOpenSSL: https://pyopenssl.org/en/stable/api.html 12 | 13 | ## Install and run 14 | 15 | ```bash 16 | > pip install flask-restful 17 | 18 | > pip install pyopenssl 19 | 20 | > python3 koreader-flask.py --help 21 | 22 | ``` 23 | 24 | ## Modify according to your needs 25 | 26 | Do whatever you want to 27 | -------------------------------------------------------------------------------- /koreader-flask.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import json 4 | import argparse 5 | import logging 6 | import traceback 7 | from logging.handlers import RotatingFileHandler 8 | from flask import Flask, request, jsonify 9 | 10 | # TODO return content type application/vnd.koreader.v1+json 11 | 12 | ## Database stuff. Easy to replace with anything: MySql, sqlite, whatever... 13 | 14 | # UserDB: 15 | # { 16 | # 'username' : { 17 | # 'username': "username", // Duplicated for convenience 18 | # 'userkey' : "key", 19 | # 'documents' : { 20 | # 'document' : { 21 | # 'progress' : "progress", 22 | # 'percentage' : percentage, 23 | # 'device' : "device", 24 | # 'device_id': "deviceId", 25 | # 'timestamp': timestamp 26 | # } 27 | # } 28 | #} 29 | 30 | def loadDb(): 31 | global users 32 | 33 | if os.path.isfile(USERSDB): 34 | theFile = open(USERSDB, 'r', encoding='utf-8') 35 | users = json.load(theFile) 36 | theFile.close() 37 | else: 38 | users = dict() 39 | return users; 40 | 41 | def saveDb(): 42 | global users 43 | 44 | theFile = open(USERSDB, 'w+', encoding='utf-8') 45 | print(json.dumps(users, indent=2), file=theFile) 46 | theFile.close() 47 | 48 | def getUser(userName): 49 | global users 50 | 51 | loadDb() 52 | return users.get(userName) 53 | 54 | def addUser(userName, userKey): 55 | global users 56 | 57 | if (getUser(userName) != None): 58 | return False 59 | users[userName] = dict(username=userName, userkey=userKey) 60 | saveDb() 61 | return True 62 | 63 | def getPosition(username, document): 64 | user = getUser(username) 65 | doc = dict(); 66 | documents = user.get('documents') 67 | if (documents != None): 68 | doc = documents.get(document) 69 | if (doc != None): 70 | doc['document'] = document 71 | return doc 72 | 73 | def updatePosition(username, document, position): 74 | user = getUser(username) 75 | doc = dict() 76 | timestamp = int(time.time()) 77 | doc['percentage'] = position.get('percentage') 78 | doc['progress'] = position.get('progress') 79 | doc['device'] = position.get('device') 80 | doc['device_id'] = position.get('device_id') 81 | doc['timestamp'] = timestamp 82 | 83 | if (user.get('documents') == None): 84 | user['documents'] = dict() 85 | user['documents'][document] = doc 86 | saveDb() 87 | return timestamp 88 | 89 | ### Database stuff ends here 90 | 91 | # Web Server stuff 92 | 93 | app = Flask(__name__) 94 | 95 | class ServiceError(Exception): 96 | status_code = 400 97 | 98 | def __init__(self, message, status_code=None, payload=None): 99 | Exception.__init__(self) 100 | super(ServiceError, self).__init__(message) 101 | if status_code is not None: 102 | self.status_code = status_code 103 | self.payload = payload 104 | 105 | def to_dict(self): 106 | thePayload = self.payload 107 | if (thePayload): 108 | # Convert non-iterable payload to an iterable 109 | try: 110 | iter(thePayload) 111 | except: 112 | thePayload = (thePayload) 113 | rv = dict(thePayload or ()) 114 | rv['message'] = str(self) 115 | return rv 116 | 117 | def logException(exception): 118 | app.logger.error("".join(traceback.format_exception(type(exception), exception, exception.__traceback__))) 119 | 120 | @app.errorhandler(ServiceError) 121 | def handle_service_error(error): 122 | logException(error) 123 | response = jsonify(error.to_dict()) 124 | response.status_code = error.status_code 125 | return response 126 | 127 | def authorizeRequest(request): 128 | username = request.headers.get("x-auth-user") 129 | userkey = request.headers.get("x-auth-key") 130 | if (username == None or userkey == None): 131 | raise ServiceError('Unauthorized', status_code=401) 132 | 133 | user = getUser(username) 134 | if (user == None): 135 | raise ServiceError('Forbidden', status_code=403) 136 | if (userkey != user['userkey']): 137 | raise ServiceError('Unauthorized', status_code=401) 138 | return user 139 | 140 | # API 141 | 142 | @app.route('/users/create', methods = ['POST']) 143 | def register(): 144 | try: 145 | if (request.is_json): 146 | user = request.get_json() 147 | username = user.get('username') 148 | userkey = user.get('password') 149 | if (username == None or userkey == None): 150 | return 'Invalid request', 400 151 | if (not addUser(username, userkey)): 152 | return 'Username is already registered.', 409 153 | return jsonify(dict(username=username)), 201 154 | else: 155 | return 'Invalid request', 400 156 | except Exception as e: 157 | raise ServiceError('Unknown server error', status_code=500) from e 158 | 159 | @app.route('/users/auth') 160 | def authorize(): 161 | try: 162 | authorizeRequest(request) 163 | return jsonify(dict(authorized='OK')), 200 164 | except ServiceError as se: 165 | raise 166 | except Exception as e: 167 | raise ServiceError('Unknown server error', status_code=500) from e 168 | 169 | @app.route('/syncs/progress/') 170 | def getProgress(document): 171 | try: 172 | user = authorizeRequest(request) 173 | position = getPosition(user['username'], document) 174 | return jsonify(position), 200 175 | except ServiceError as se: 176 | raise 177 | except Exception as e: 178 | raise ServiceError('Unknown server error', status_code=500) from e 179 | 180 | @app.route('/syncs/progress', methods = ['PUT']) 181 | def updateProgress(): 182 | try: 183 | user = authorizeRequest(request) 184 | if (request.is_json): 185 | position = request.get_json() 186 | document = position.get('document') 187 | timestamp = updatePosition(user['username'], document, position) 188 | return jsonify(dict(document = document, timestamp = timestamp)), 200 189 | else: 190 | return 'Invalid request', 400 191 | except ServiceError as se: 192 | raise 193 | except Exception as e: 194 | raise ServiceError('Unknown server error', status_code=500) from e 195 | 196 | # Initialization 197 | 198 | def main(): 199 | parser = argparse.ArgumentParser(description="KOReader Sync Server") 200 | parser.add_argument("-d", "--database", type = str, default='users.json', help = "JSON Database file") 201 | parser.add_argument("-t", "--host", type = str, default="0.0.0.0", help = "Server host") 202 | parser.add_argument("-p", "--port", type = int, default=8081, help = "Server port") 203 | parser.add_argument("-c", "--certificate", type = str, help = "SSL Certificate file") 204 | parser.add_argument("-k", "--key", type = str, help = "SSL Private key file") 205 | parser.add_argument("-l", "--logfile", type = str, default='koreader-server.log', help = "Log file") 206 | parser.add_argument("-v", "--verbose", action='store_true', help = "Run server in debug mode") 207 | args = parser.parse_args() 208 | 209 | global USERSDB 210 | 211 | USERSDB = args.database 212 | handler = RotatingFileHandler(args.logfile, maxBytes=100000, backupCount=1) 213 | handler.setLevel(logging.DEBUG) 214 | logging.getLogger('werkzeug').addHandler(handler) # HTTP log goes here 215 | app.logger.addHandler(handler) 216 | app.logger.setLevel(logging.DEBUG) 217 | 218 | context = None 219 | if (args.certificate and args.key): 220 | context = (args.certificate, args.key) 221 | app.run(debug=args.verbose,host=args.host, port=args.port, ssl_context=context) 222 | 223 | main() 224 | --------------------------------------------------------------------------------