diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index 7c84854513..b291fa164e 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -1,6 +1,6 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import os, re +import os, re, time from PyQt4.QtGui import QDialog, QMessageBox, QListWidgetItem, QIcon, \ QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit @@ -29,7 +29,7 @@ class ConfigDialog(QDialog, Ui_Dialog): self.item3 = QListWidgetItem(QIcon(':/images/view.svg'), _('Advanced'), self.category_list) self.item4 = QListWidgetItem(QIcon(':/images/network-server.svg'), _('Content\nServer'), self.category_list) self.db = db - self.server = None + self.server = server path = prefs['library_path'] self.location.setText(path if path else '') self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse) @@ -104,7 +104,6 @@ class ConfigDialog(QDialog, Ui_Dialog): self.viewer.item(self.viewer.count()-1).setCheckState(Qt.Checked if ext.upper() in config['internally_viewed_formats'] else Qt.Unchecked) added_html = ext == 'html' self.viewer.sortItems() - self.start.setEnabled(not getattr(self.server, 'is_running', False)) self.test.setEnabled(not self.start.isEnabled()) self.stop.setDisabled(self.start.isEnabled()) @@ -159,6 +158,12 @@ class ConfigDialog(QDialog, Ui_Dialog): self.set_server_options() from calibre.library.server import start_threaded_server self.server = start_threaded_server(self.db, server_config().parse()) + while not self.server.is_running and self.server.exception is None: + time.sleep(1) + if self.server.exception is not None: + error_dialog(self, _('Failed to start content server'), + unicode(self.server.exception)).exec_() + return self.start.setEnabled(False) self.test.setEnabled(True) self.stop.setEnabled(True) diff --git a/src/calibre/gui2/images/network-server.svg b/src/calibre/gui2/images/network-server.svg new file mode 100644 index 0000000000..80f54e090d --- /dev/null +++ b/src/calibre/gui2/images/network-server.svg @@ -0,0 +1,4456 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + OXYGEN + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index f0cb9f330b..b64a002a71 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -529,7 +529,7 @@ class BooksView(TableView): #QObject.connect(self.model(), SIGNAL('rowsRemoved(QModelIndex, int, int)'), self.resizeRowsToContents) #QObject.connect(self.model(), SIGNAL('rowsInserted(QModelIndex, int, int)'), self.resizeRowsToContents) self.set_visible_columns() - + def columns_sorted(self): if self.__class__.__name__ == 'BooksView': try: diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index e1a43d7663..0cd5c3e3ff 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal ' import os, sys, textwrap, collections, traceback, time, re from xml.parsers.expat import ExpatError from functools import partial -from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl +from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \ QToolButton, QDialog, QDesktopServices, QFileDialog from PyQt4.QtSvg import QSvgRenderer @@ -287,8 +287,13 @@ class Main(MainWindow, Ui_MainWindow): if config['autolaunch_server']: from calibre.library.server import start_threaded_server from calibre.library import server_config - self.server = start_threaded_server(db, server_config().parse()) + self.content_server = start_threaded_server(db, server_config().parse()) + self.test_server_timer = QTimer.singleShot(10000, self.test_server) + def test_server(self, *args): + if self.content_server.exception is not None: + error_dialog(self, _('Failed to start content server'), + unicode(self.content_server.exception)).exec_() def toggle_cover_flow(self, show): if show: @@ -1011,7 +1016,7 @@ class Main(MainWindow, Ui_MainWindow): d = error_dialog(self, _('Cannot configure'), _('Cannot configure while there are running jobs.')) d.exec_() return - d = ConfigDialog(self, self.library_view.model().db, self.content_server) + d = ConfigDialog(self, self.library_view.model().db, server=self.content_server) d.exec_() self.content_server = d.server if d.result() == d.Accepted: @@ -1222,11 +1227,14 @@ in which you want to store your books files. Any existing books will be automati self.hide() self.cover_cache.terminate() try: - if self.server is not None: - self.server.exit() - except: + try: + if self.content_server is not None: + self.content_server.exit() + except: + pass + time.sleep(2) + except KeyboardInterrupt: pass - time.sleep(2) e.accept() def update_found(self, version): diff --git a/src/calibre/library/__init__.py b/src/calibre/library/__init__.py index 46fa4362f2..88c022d805 100644 --- a/src/calibre/library/__init__.py +++ b/src/calibre/library/__init__.py @@ -2,6 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' ''' Code to manage ebook library''' import re +from calibre.utils.config import Config, StringConfig title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE) def title_sort(title): @@ -9,4 +10,24 @@ def title_sort(title): if match: prep = match.group(1) title = title.replace(prep, '') + ', ' + prep - return title.strip() \ No newline at end of file + return title.strip() + +def server_config(defaults=None): + desc=_('Settings to control the calibre content server') + c = Config('server', desc) if defaults is None else StringConfig(defaults, desc) + + c.add_opt('port', ['-p', '--port'], default=8080, + help=_('The port on which to listen. Default is %default')) + c.add_opt('timeout', ['-t', '--timeout'], default=120, + help=_('The server timeout in seconds. Default is %default')) + c.add_opt('thread_pool', ['--thread-pool'], default=30, + help=_('The max number of worker threads to use. Default is %default')) + c.add_opt('hostname', ['--hostname'], default='localhost', + help=_('The hostname of the machine the server is running on. Used when generating the stanza feeds. Default is %default')) + c.add_opt('password', ['--password'], default=None, + help=_('Set a password to restrict access. By default access is unrestricted.')) + c.add_opt('username', ['--username'], default='calibre', + help=_('Username for access. By default, it is: %default')) + c.add_opt('develop', ['--develop'], default=False, + help='Development mode. Server automatically restarts on file changes and serves code files (html, css, js) from the file system instead of calibre\'s resource system.') + return c diff --git a/src/calibre/library/server.py b/src/calibre/library/server.py index 9cf15e2680..a967195037 100644 --- a/src/calibre/library/server.py +++ b/src/calibre/library/server.py @@ -7,17 +7,21 @@ __docformat__ = 'restructuredtext en' HTTP server for remote access to the calibre database. ''' -import sys, textwrap, cStringIO, mimetypes, operator, os, re +import sys, textwrap, cStringIO, mimetypes, operator, os, re, logging +from logging.handlers import RotatingFileHandler from datetime import datetime +from threading import Thread + import cherrypy from PIL import Image from calibre.constants import __version__, __appname__ -from calibre.utils.config import StringConfig, Config from calibre.utils.genshi.template import MarkupTemplate from calibre import fit_image from calibre.resources import jquery, server_resources, build_time +from calibre.library import server_config as config from calibre.library.database2 import LibraryDatabase2, FIELD_MAP +from calibre.utils.config import config_dir build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S') server_resources['jquery.js'] = jquery @@ -30,6 +34,10 @@ def expose(func): return cherrypy.expose(do) +log_access_file = os.path.join(config_dir, 'server_access_log.txt') +log_error_file = os.path.join(config_dir, 'server_error_log.txt') + + class LibraryServer(object): server_name = __appname__ + '/' + __version__ @@ -97,7 +105,7 @@ class LibraryServer(object): ''')) - def __init__(self, db, opts): + def __init__(self, db, opts, embedded=False, show_tracebacks=True): self.db = db for item in self.db: item @@ -105,20 +113,68 @@ class LibraryServer(object): self.opts = opts cherrypy.config.update({ - 'server.socket_host': '0.0.0.0', - 'server.socket_port': opts.port, - 'server.socket_timeout': opts.timeout, #seconds - 'server.thread_pool': opts.thread_pool, # number of threads + 'log.screen' : opts.develop, + 'engine.autoreload_on' : opts.develop, + 'tools.log_headers.on' : opts.develop, + 'checker.on' : opts.develop, + 'request.show_tracebacks': show_tracebacks, + 'server.socket_host' : '0.0.0.0', + 'server.socket_port' : opts.port, + 'server.socket_timeout' : opts.timeout, #seconds + 'server.thread_pool' : opts.thread_pool, # number of threads }) - self.config = textwrap.dedent('''\ - [global] - engine.autoreload_on = %(autoreload)s - tools.gzip.on = True - tools.gzip.mime_types = ['text/html', 'text/plain', 'text/xml', 'text/javascript', 'text/css'] - ''')%dict(autoreload=opts.develop) + if embedded: + cherrypy.config.update({'engine.SIGHUP' : None, + 'engine.SIGTERM' : None,}) + self.config = {'global': { + 'tools.gzip.on' : True, + 'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/xml', 'text/javascript', 'text/css'], + }} + if opts.password: + g = self.config['global'] + g['tools.digest_auth.on'] = True + g['tools.digest_auth.realm'] = _('Password to access your calibre library. Username is ') + opts.username.strip() + g['tools.digest_auth.users'] = {opts.username.strip():opts.password.strip()} + + self.is_running = False + self.exception = None + def setup_loggers(self): + access_file = log_access_file + error_file = log_error_file + log = cherrypy.log + + maxBytes = getattr(log, "rot_maxBytes", 10000000) + backupCount = getattr(log, "rot_backupCount", 1000) + + # Make a new RotatingFileHandler for the error log. + h = RotatingFileHandler(error_file, 'a', maxBytes, backupCount) + h.setLevel(logging.DEBUG) + h.setFormatter(cherrypy._cplogging.logfmt) + log.error_log.addHandler(h) + + # Make a new RotatingFileHandler for the access log. + h = RotatingFileHandler(access_file, 'a', maxBytes, backupCount) + h.setLevel(logging.DEBUG) + h.setFormatter(cherrypy._cplogging.logfmt) + log.access_log.addHandler(h) + + def start(self): - cherrypy.quickstart(self, config=cStringIO.StringIO(self.config)) + self.is_running = False + self.setup_loggers() + cherrypy.tree.mount(self, '', config=self.config) + try: + cherrypy.engine.start() + self.is_running = True + cherrypy.engine.block() + except Exception, e: + self.exception = e + finally: + self.is_running = False + + def exit(self): + cherrypy.engine.exit() def get_cover(self, id, thumbnail=False): cover = self.db.cover(id, index_is_id=True, as_file=True) @@ -283,26 +339,18 @@ class LibraryServer(object): if server_resources.has_key(name): return server_resources[name] raise cherrypy.HTTPError(404, '%s not found'%name) - - - -def config(defaults=None): - desc=_('Settings to control the calibre content server') - c = Config('server', desc) if defaults is None else StringConfig(defaults, desc) - - c.add_opt('port', ['-p', '--port'], default=8080, - help=_('The port on which to listen. Default is %default')) - c.add_opt('timeout', ['-t', '--timeout'], default=120, - help=_('The server timeout in seconds. Default is %default')) - c.add_opt('thread_pool', ['--thread-pool'], default=30, - help=_('The max number of worker threads to use. Default is %default')) - c.add_opt('hostname', ['--hostname'], default='localhost', - help=_('The hostname of the machine the server is running on. Used when generating the stanza feeds. Default is %default')) - - c.add_opt('develop', ['--develop'], default=False, - help='Development mode. Server automatically restarts on file changes and serves code files (html, css, js) from the file system instead of calibre\'s resource system.') - return c +def start_threaded_server(db, opts): + server = LibraryServer(db, opts, embedded=True) + server.thread = Thread(target=server.start) + server.thread.setDaemon(True) + server.thread.start() + return server + +def stop_threaded_server(server): + server.exit() + server.thread = None + def option_parser(): return config().option_parser('%prog '+ _('[options]\n\nStart the calibre content server.'))