mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Start work on embedding the new content server in the GUI
This commit is contained in:
parent
420841377c
commit
3c64011526
@ -316,6 +316,11 @@ class Connection(apsw.Connection): # {{{
|
||||
# }}}
|
||||
|
||||
|
||||
def set_global_state(backend):
|
||||
load_user_template_functions(backend.library_id,
|
||||
backend.prefs.get('user_template_functions', []))
|
||||
|
||||
|
||||
class DB(object):
|
||||
|
||||
PATH_LIMIT = 40 if iswindows else 100
|
||||
@ -402,8 +407,7 @@ class DB(object):
|
||||
self.initialize_custom_columns()
|
||||
self.initialize_tables()
|
||||
if load_user_formatter_functions:
|
||||
load_user_template_functions(self.library_id,
|
||||
self.prefs.get('user_template_functions', []))
|
||||
set_global_state(self)
|
||||
|
||||
def initialize_prefs(self, default_prefs, restore_all_prefs, progress_callback): # {{{
|
||||
self.prefs = DBPrefs(self)
|
||||
|
@ -15,7 +15,7 @@ from calibre.db import _get_next_series_num_for_list, _get_series_values, get_da
|
||||
from calibre.db.adding import (
|
||||
find_books_in_directory, import_book_directory_multiple,
|
||||
import_book_directory, recursive_import, add_catalog, add_news)
|
||||
from calibre.db.backend import DB
|
||||
from calibre.db.backend import DB, set_global_state as backend_set_global_state
|
||||
from calibre.db.cache import Cache
|
||||
from calibre.db.errors import NoSuchFormat
|
||||
from calibre.db.categories import CATEGORY_SORTS
|
||||
@ -48,6 +48,11 @@ def create_backend(
|
||||
load_user_formatter_functions=load_user_formatter_functions)
|
||||
|
||||
|
||||
def set_global_state(db):
|
||||
backend_set_global_state(db.backend)
|
||||
set_saved_searches(db, 'saved_searches')
|
||||
|
||||
|
||||
class LibraryDatabase(object):
|
||||
|
||||
''' Emulate the old LibraryDatabase2 interface '''
|
||||
@ -92,14 +97,16 @@ class LibraryDatabase(object):
|
||||
set_saved_searches(self, 'saved_searches')
|
||||
|
||||
def close(self):
|
||||
self.new_api.close()
|
||||
if hasattr(self, 'new_api'):
|
||||
self.new_api.close()
|
||||
|
||||
def break_cycles(self):
|
||||
delattr(self.backend, 'field_metadata')
|
||||
self.data.cache.backend = None
|
||||
self.data.cache = None
|
||||
for x in ('data', 'backend', 'new_api', 'listeners',):
|
||||
delattr(self, x)
|
||||
if hasattr(self, 'backend'):
|
||||
delattr(self.backend, 'field_metadata')
|
||||
self.data.cache.backend = None
|
||||
self.data.cache = None
|
||||
for x in ('data', 'backend', 'new_api', 'listeners',):
|
||||
delattr(self, x)
|
||||
|
||||
# Library wide properties {{{
|
||||
@property
|
||||
@ -742,6 +749,7 @@ class LibraryDatabase(object):
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
MT = lambda func: types.MethodType(func, None, LibraryDatabase)
|
||||
|
||||
# Legacy getter API {{{
|
||||
@ -936,4 +944,3 @@ LibraryDatabase.commit = MT(lambda self:None)
|
||||
# }}}
|
||||
|
||||
del MT
|
||||
|
||||
|
@ -421,6 +421,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
'Try switching to this library first, then switch back '
|
||||
'and retry the renaming.')%loc, show=True)
|
||||
return
|
||||
self.gui.library_broker.remove_library(loc)
|
||||
try:
|
||||
os.rename(loc, newloc)
|
||||
except:
|
||||
@ -448,6 +449,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
yes_text=_('&OK'), no_text=_('&Undo'), yes_icon='ok.png', no_icon='edit-undo.png'):
|
||||
return
|
||||
self.stats.remove(location)
|
||||
self.gui.library_broker.remove_library(location)
|
||||
self.build_menus()
|
||||
self.gui.iactions['Copy To Library'].build_menus()
|
||||
if os.path.exists(loc):
|
||||
@ -484,7 +486,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
db = m.db
|
||||
db.prefs.disable_setting = True
|
||||
if restore_database(db, self.gui):
|
||||
self.gui.library_moved(db.library_path, call_close=False)
|
||||
self.gui.library_moved(db.library_path)
|
||||
|
||||
def check_library(self):
|
||||
from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck
|
||||
@ -502,7 +504,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
except:
|
||||
pass
|
||||
d.break_cycles()
|
||||
self.gui.library_moved(library_path, call_close=False)
|
||||
self.gui.library_moved(library_path)
|
||||
if d.rejected:
|
||||
return
|
||||
if d.error is None:
|
||||
|
@ -7,59 +7,127 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import time
|
||||
|
||||
from PyQt5.Qt import Qt, QUrl, QDialog, QSize, QVBoxLayout, QLabel, \
|
||||
QPlainTextEdit, QDialogButtonBox, QTimer
|
||||
from PyQt5.Qt import (
|
||||
QCheckBox, QDialog, QDialogButtonBox, QLabel, QPlainTextEdit, QSize, Qt,
|
||||
QTabWidget, QTimer, QUrl, QVBoxLayout, QWidget, pyqtSignal, QHBoxLayout,
|
||||
QPushButton
|
||||
)
|
||||
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||
from calibre.gui2.preferences.server_ui import Ui_Form
|
||||
from calibre.library.server import server_config
|
||||
from calibre.utils.config import ConfigProxy
|
||||
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
|
||||
Dispatcher, info_dialog
|
||||
from calibre import as_unicode
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.gui2 import (
|
||||
Dispatcher, config, error_dialog, info_dialog, open_url, warning_dialog
|
||||
)
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||
from calibre.srv.opts import server_config, options
|
||||
|
||||
|
||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
class MainTab(QWidget):
|
||||
|
||||
changed_signal = pyqtSignal()
|
||||
start_server = pyqtSignal()
|
||||
stop_server = pyqtSignal()
|
||||
test_server = pyqtSignal()
|
||||
show_logs = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.l = l = QVBoxLayout(self)
|
||||
self.la = la = QLabel(_(
|
||||
'calibre contains an internet server that allows you to'
|
||||
' access your book collection using a browser from anywhere'
|
||||
' in the world. Any changes to the settings will only take'
|
||||
' effect after a server restart.'))
|
||||
la.setWordWrap(True)
|
||||
l.addWidget(la)
|
||||
l.addSpacing(10)
|
||||
self.opt_auth = cb = QCheckBox(_('Require username/password to access the content server'))
|
||||
l.addWidget(cb)
|
||||
self.auth_desc = la = QLabel(self)
|
||||
la.setStyleSheet('QLabel { font-size: smaller }')
|
||||
la.setWordWrap(True)
|
||||
l.addSpacing(25)
|
||||
self.opt_autolaunch_server = al = QCheckBox(_('Run server &automatically when calibre starts'))
|
||||
l.addWidget(al)
|
||||
self.h = h = QHBoxLayout()
|
||||
l.addLayout(h)
|
||||
for text, name in [(_('&Start server'), 'start_server'), (_('St&op server'), 'stop_server'),
|
||||
(_('&Test server'), 'test_server'), (_('Show server &logs'), 'show_logs')]:
|
||||
b = QPushButton(text)
|
||||
b.clicked.connect(getattr(self, name).emit)
|
||||
setattr(self, name + '_button', b)
|
||||
if name == 'show_logs':
|
||||
h.addStretch(10)
|
||||
h.addWidget(b)
|
||||
|
||||
def genesis(self):
|
||||
opts = server_config()
|
||||
self.opt_auth.setChecked(opts.auth)
|
||||
self.opt_auth.stateChanged.connect(self.auth_changed)
|
||||
self.change_auth_desc()
|
||||
self.update_button_state()
|
||||
|
||||
def change_auth_desc(self):
|
||||
self.auth_desc.setText(
|
||||
_('Remember to create some user accounts in the "Users" tab') if self.opt_auth.isChecked() else
|
||||
_('Requiring a username/password prevents unauthorized people from'
|
||||
' accessing your calibre library. It is also needed for some features'
|
||||
' such as last read position/annotation syncing and making'
|
||||
' changes to the library.')
|
||||
)
|
||||
|
||||
def auth_changed(self):
|
||||
self.changed_signal.emit()
|
||||
self.change_auth_desc()
|
||||
|
||||
def restore_defaults(self):
|
||||
self.auth_changed.setChecked(options['auth'].default)
|
||||
|
||||
def update_button_state(self):
|
||||
gui = self.parent().gui
|
||||
is_running = gui.content_server is not None and gui.content_server.is_running
|
||||
self.start_server_button.setEnabled(not is_running)
|
||||
self.stop_server_button.setEnabled(is_running)
|
||||
self.test_server_button.setEnabled(is_running)
|
||||
|
||||
|
||||
class ConfigWidget(ConfigWidgetBase):
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
ConfigWidgetBase.__init__(self, *args, **kw)
|
||||
self.l = l = QVBoxLayout(self)
|
||||
l.setContentsMargins(0, 0, 0, 0)
|
||||
self.tabs_widget = t = QTabWidget(self)
|
||||
self.main_tab = m = MainTab(self)
|
||||
t.addTab(m, _('Main'))
|
||||
m.start_server.connect(self.start_server)
|
||||
m.stop_server.connect(self.stop_server)
|
||||
m.test_server.connect(self.test_server)
|
||||
m.show_logs.connect(self.view_server_logs)
|
||||
self.opt_autolaunch_server = m.opt_autolaunch_server
|
||||
for tab in self.tabs:
|
||||
if hasattr(tab, 'changed_signal'):
|
||||
tab.changed_signal.connect(self.changed_signal.emit)
|
||||
|
||||
@property
|
||||
def tabs(self):
|
||||
return (self.tabs_widget.widget(i) for i in range(self.tabs_widget.count()))
|
||||
|
||||
@property
|
||||
def server(self):
|
||||
return self.gui.server
|
||||
|
||||
def restore_defaults(self):
|
||||
ConfigWidgetBase.restore_defaults(self)
|
||||
for tab in self.tabs:
|
||||
if hasattr(tab, 'restore_defaults'):
|
||||
tab.restore_defaults()
|
||||
|
||||
def genesis(self, gui):
|
||||
self.gui = gui
|
||||
self.proxy = ConfigProxy(server_config())
|
||||
db = self.db = gui.library_view.model().db
|
||||
self.server = self.gui.content_server
|
||||
for tab in self.tabs:
|
||||
tab.genesis()
|
||||
|
||||
r = self.register
|
||||
|
||||
r('port', self.proxy)
|
||||
r('username', self.proxy)
|
||||
r('password', self.proxy)
|
||||
r('max_cover', self.proxy)
|
||||
r('max_opds_items', self.proxy)
|
||||
r('max_opds_ungrouped_items', self.proxy)
|
||||
r('url_prefix', self.proxy)
|
||||
|
||||
self.show_server_password.stateChanged[int].connect(
|
||||
lambda s: self.opt_password.setEchoMode(
|
||||
self.opt_password.Normal if s == Qt.Checked
|
||||
else self.opt_password.Password))
|
||||
self.opt_password.setEchoMode(self.opt_password.Password)
|
||||
|
||||
restrictions = sorted(db.prefs['virtual_libraries'].iterkeys(), key=sort_key)
|
||||
choices = [('', '')] + [(x, x) for x in restrictions]
|
||||
# check that the virtual library still exists
|
||||
vls = db.prefs['cs_virtual_lib_on_startup']
|
||||
if vls and vls not in restrictions:
|
||||
db.prefs['cs_virtual_lib_on_startup'] = ''
|
||||
r('cs_virtual_lib_on_startup', db.prefs, choices=choices)
|
||||
|
||||
self.start_button.setEnabled(not getattr(self.server, 'is_running', False))
|
||||
self.test_button.setEnabled(not self.start_button.isEnabled())
|
||||
self.stop_button.setDisabled(self.start_button.isEnabled())
|
||||
self.start_button.clicked.connect(self.start_server)
|
||||
self.stop_button.clicked.connect(self.stop_server)
|
||||
self.test_button.clicked.connect(self.test_server)
|
||||
self.view_logs.clicked.connect(self.view_server_logs)
|
||||
|
||||
r('autolaunch_server', config)
|
||||
|
||||
def start_server(self):
|
||||
@ -74,9 +142,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
error_dialog(self, _('Failed to start content server'),
|
||||
as_unicode(self.gui.content_server.exception)).exec_()
|
||||
return
|
||||
self.start_button.setEnabled(False)
|
||||
self.test_button.setEnabled(True)
|
||||
self.stop_button.setEnabled(True)
|
||||
self.main_tab.update_button_state()
|
||||
finally:
|
||||
self.unsetCursor()
|
||||
|
||||
@ -94,9 +160,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
return
|
||||
|
||||
self.gui.content_server = None
|
||||
self.start_button.setEnabled(True)
|
||||
self.test_button.setEnabled(False)
|
||||
self.stop_button.setEnabled(False)
|
||||
self.main_tab.update_button_state()
|
||||
self.stopping_msg.accept()
|
||||
|
||||
def test_server(self):
|
||||
@ -104,7 +168,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
open_url(QUrl('http://127.0.0.1:'+str(self.opt_port.value())+prefix))
|
||||
|
||||
def view_server_logs(self):
|
||||
from calibre.library.server import log_access_file, log_error_file
|
||||
from calibre.srv.embedded import log_paths
|
||||
log_error_file, log_access_file = log_paths()
|
||||
d = QDialog(self)
|
||||
d.resize(QSize(800, 600))
|
||||
layout = QVBoxLayout()
|
||||
@ -113,15 +178,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
el = QPlainTextEdit(d)
|
||||
layout.addWidget(el)
|
||||
try:
|
||||
el.setPlainText(open(log_error_file, 'rb').read().decode('utf8', 'replace'))
|
||||
except IOError:
|
||||
el.setPlainText(lopen(log_error_file, 'rb').read().decode('utf8', 'replace'))
|
||||
except EnvironmentError:
|
||||
el.setPlainText('No error log found')
|
||||
layout.addWidget(QLabel(_('Access log:')))
|
||||
al = QPlainTextEdit(d)
|
||||
layout.addWidget(al)
|
||||
try:
|
||||
al.setPlainText(open(log_access_file, 'rb').read().decode('utf8', 'replace'))
|
||||
except IOError:
|
||||
al.setPlainText(lopen(log_access_file, 'rb').read().decode('utf8', 'replace'))
|
||||
except EnvironmentError:
|
||||
al.setPlainText('No access log found')
|
||||
bx = QDialogButtonBox(QDialogButtonBox.Ok)
|
||||
layout.addWidget(bx)
|
||||
@ -147,4 +212,3 @@ if __name__ == '__main__':
|
||||
from PyQt5.Qt import QApplication
|
||||
app = QApplication([])
|
||||
test_widget('Sharing', 'Server')
|
||||
|
||||
|
@ -1,303 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>641</width>
|
||||
<height>563</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="1" column="1" colspan="2">
|
||||
<widget class="QLineEdit" name="opt_username"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_12">
|
||||
<property name="text">
|
||||
<string>&Password:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_password</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="opt_password">
|
||||
<property name="toolTip">
|
||||
<string><p>If you leave the password blank, anyone will be able to
|
||||
access your book collection using the web interface.
|
||||
<br>
|
||||
<p>Some devices have browsers that do not support authentication. If you are having trouble downloading files from the content server, try removing the password.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1" colspan="2">
|
||||
<widget class="QLineEdit" name="opt_max_cover">
|
||||
<property name="toolTip">
|
||||
<string>The maximum size (widthxheight) for displayed covers. Larger covers are resized. </string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Max. &cover size:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_max_cover</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string>Server &port:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_port</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QSpinBox" name="opt_port">
|
||||
<property name="maximum">
|
||||
<number>65535</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>8080</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string>&Username:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_username</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1" colspan="2">
|
||||
<widget class="QCheckBox" name="show_server_password">
|
||||
<property name="text">
|
||||
<string>&Show password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_15">
|
||||
<property name="text">
|
||||
<string>Max. &OPDS items per query:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_max_opds_items</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1" colspan="2">
|
||||
<widget class="QSpinBox" name="opt_max_opds_items">
|
||||
<property name="minimum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1" colspan="2">
|
||||
<widget class="QSpinBox" name="opt_max_opds_ungrouped_items">
|
||||
<property name="minimum">
|
||||
<number>25</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_16">
|
||||
<property name="text">
|
||||
<string>Max. &ungrouped items:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_max_opds_ungrouped_items</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_164">
|
||||
<property name="text">
|
||||
<string>Virtual library to apply:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1" colspan="2">
|
||||
<widget class="QComboBox" name="opt_cs_virtual_lib_on_startup">
|
||||
<property name="toolTip">
|
||||
<string>Setting a virtual library will restrict the books the content server makes available to those in the library. This setting is per library (i.e. you can have a different value per library).</string>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
||||
</property>
|
||||
<property name="minimumContentsLength">
|
||||
<number>20</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><p>Some devices have browsers that do not support authentication. If you are having trouble downloading files from the content server, trying removing the password.</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QLabel {color:red}</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Passwords are incompatible with some devices</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>&URL Prefix:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_url_prefix</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1" colspan="2">
|
||||
<widget class="QLineEdit" name="opt_url_prefix">
|
||||
<property name="toolTip">
|
||||
<string>A prefix that is applied to all URLs in the content server. Useful only if you plan to put the server behind another server like Apache, with a reverse proxy.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="start_button">
|
||||
<property name="text">
|
||||
<string>&Start Server</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="stop_button">
|
||||
<property name="text">
|
||||
<string>St&op Server</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="test_button">
|
||||
<property name="text">
|
||||
<string>&Test Server</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>calibre contains a network server that allows you to access your book collection using a browser from anywhere in the world. Any changes to the settings will only take effect after a server restart.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_autolaunch_server">
|
||||
<property name="text">
|
||||
<string>Run server &automatically when calibre starts</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="view_logs">
|
||||
<property name="text">
|
||||
<string>View &server logs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>36</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_13">
|
||||
<property name="text">
|
||||
<string><p>Remember to leave calibre running as the server only runs as long as calibre is running. If you wish to run the server as a standalone entity, please refer to the <a href="https://manual.calibre-ebook.com/generated/en/calibre-server.html">command-line documentation</a>.
|
||||
<p>To connect to the calibre server from your device you should use a URL of the form <b>http://myhostname:8080</b>. Here myhostname should be either the fully qualified hostname or the IP address of the computer calibre is running on. If you want to access the server from anywhere in the world, you will have to setup port forwarding for it on your router.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>37</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -51,6 +51,7 @@ from calibre.gui2.job_indicator import Pointer
|
||||
from calibre.gui2.dbus_export.widgets import factory
|
||||
from calibre.gui2.open_with import register_keyboard_shortcuts
|
||||
from calibre.library import current_library_name
|
||||
from calibre.srv.library_broker import GuiLibraryBroker
|
||||
|
||||
|
||||
class Listener(Thread): # {{{
|
||||
@ -220,6 +221,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
opts = self.opts
|
||||
self.preferences_action, self.quit_action = actions
|
||||
self.library_path = library_path
|
||||
self.library_broker = GuiLibraryBroker(db)
|
||||
self.content_server = None
|
||||
self._spare_pool = None
|
||||
self.must_restart_before_config = False
|
||||
@ -626,81 +628,76 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
def booklists(self):
|
||||
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
||||
|
||||
def library_moved(self, newloc, copy_structure=False, call_close=True,
|
||||
allow_rebuild=False):
|
||||
def library_moved(self, newloc, copy_structure=False, allow_rebuild=False):
|
||||
if newloc is None:
|
||||
return
|
||||
default_prefs = None
|
||||
try:
|
||||
olddb = self.library_view.model().db
|
||||
if copy_structure:
|
||||
default_prefs = olddb.prefs
|
||||
except:
|
||||
olddb = None
|
||||
if copy_structure and olddb is not None and default_prefs is not None:
|
||||
default_prefs['field_metadata'] = olddb.new_api.field_metadata.all_metadata()
|
||||
try:
|
||||
db = LibraryDatabase(newloc, default_prefs=default_prefs)
|
||||
except apsw.Error:
|
||||
if not allow_rebuild:
|
||||
raise
|
||||
import traceback
|
||||
repair = question_dialog(self, _('Corrupted database'),
|
||||
_('The library database at %s appears to be corrupted. Do '
|
||||
'you want calibre to try and rebuild it automatically? '
|
||||
'The rebuild may not be completely successful.')
|
||||
% force_unicode(newloc, filesystem_encoding),
|
||||
det_msg=traceback.format_exc()
|
||||
)
|
||||
if repair:
|
||||
from calibre.gui2.dialogs.restore_library import repair_library_at
|
||||
if repair_library_at(newloc, parent=self):
|
||||
db = LibraryDatabase(newloc, default_prefs=default_prefs)
|
||||
else:
|
||||
return
|
||||
else:
|
||||
return
|
||||
if self.content_server is not None:
|
||||
self.content_server.set_database(db)
|
||||
self.library_path = newloc
|
||||
prefs['library_path'] = self.library_path
|
||||
self.book_on_device(None, reset=True)
|
||||
db.set_book_on_device_func(self.book_on_device)
|
||||
self.library_view.set_database(db)
|
||||
self.tags_view.set_database(db, self.alter_tb)
|
||||
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
||||
self.status_bar.clear_message()
|
||||
self.search.clear()
|
||||
self.saved_search.clear()
|
||||
self.book_details.reset_info()
|
||||
# self.library_view.model().count_changed()
|
||||
db = self.library_view.model().db
|
||||
self.iactions['Choose Library'].count_changed(db.count())
|
||||
self.set_window_title()
|
||||
self.apply_named_search_restriction('') # reset restriction to null
|
||||
self.saved_searches_changed(recount=False) # reload the search restrictions combo box
|
||||
if db.prefs['virtual_lib_on_startup']:
|
||||
self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
|
||||
self.rebuild_vl_tabs()
|
||||
for action in self.iactions.values():
|
||||
action.library_changed(db)
|
||||
if olddb is not None:
|
||||
with self.library_broker:
|
||||
default_prefs = None
|
||||
try:
|
||||
if call_close:
|
||||
olddb.close()
|
||||
olddb = self.library_view.model().db
|
||||
if copy_structure:
|
||||
default_prefs = olddb.prefs
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
olddb.break_cycles()
|
||||
if self.device_connected:
|
||||
self.set_books_in_library(self.booklists(), reset=True)
|
||||
self.refresh_ondevice()
|
||||
self.memory_view.reset()
|
||||
self.card_a_view.reset()
|
||||
self.card_b_view.reset()
|
||||
self.set_current_library_information(current_library_name(), db.library_id,
|
||||
db.field_metadata)
|
||||
self.library_view.set_current_row(0)
|
||||
olddb = None
|
||||
if copy_structure and olddb is not None and default_prefs is not None:
|
||||
default_prefs['field_metadata'] = olddb.new_api.field_metadata.all_metadata()
|
||||
db = self.library_broker.prepare_for_gui_library_change(newloc)
|
||||
if db is None:
|
||||
try:
|
||||
db = LibraryDatabase(newloc, default_prefs=default_prefs)
|
||||
except apsw.Error:
|
||||
if not allow_rebuild:
|
||||
raise
|
||||
import traceback
|
||||
repair = question_dialog(self, _('Corrupted database'),
|
||||
_('The library database at %s appears to be corrupted. Do '
|
||||
'you want calibre to try and rebuild it automatically? '
|
||||
'The rebuild may not be completely successful.')
|
||||
% force_unicode(newloc, filesystem_encoding),
|
||||
det_msg=traceback.format_exc()
|
||||
)
|
||||
if repair:
|
||||
from calibre.gui2.dialogs.restore_library import repair_library_at
|
||||
if repair_library_at(newloc, parent=self):
|
||||
db = LibraryDatabase(newloc, default_prefs=default_prefs)
|
||||
else:
|
||||
return
|
||||
else:
|
||||
return
|
||||
self.library_path = newloc
|
||||
prefs['library_path'] = self.library_path
|
||||
self.book_on_device(None, reset=True)
|
||||
db.set_book_on_device_func(self.book_on_device)
|
||||
self.library_view.set_database(db)
|
||||
self.tags_view.set_database(db, self.alter_tb)
|
||||
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
||||
self.status_bar.clear_message()
|
||||
self.search.clear()
|
||||
self.saved_search.clear()
|
||||
self.book_details.reset_info()
|
||||
# self.library_view.model().count_changed()
|
||||
db = self.library_view.model().db
|
||||
self.iactions['Choose Library'].count_changed(db.count())
|
||||
self.set_window_title()
|
||||
self.apply_named_search_restriction('') # reset restriction to null
|
||||
self.saved_searches_changed(recount=False) # reload the search restrictions combo box
|
||||
if db.prefs['virtual_lib_on_startup']:
|
||||
self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
|
||||
self.rebuild_vl_tabs()
|
||||
for action in self.iactions.values():
|
||||
action.library_changed(db)
|
||||
self.library_broker.gui_library_changed(db)
|
||||
if olddb is not None:
|
||||
olddb.close(), olddb.break_cycles()
|
||||
if self.device_connected:
|
||||
self.set_books_in_library(self.booklists(), reset=True)
|
||||
self.refresh_ondevice()
|
||||
self.memory_view.reset()
|
||||
self.card_a_view.reset()
|
||||
self.card_b_view.reset()
|
||||
self.set_current_library_information(current_library_name(), db.library_id,
|
||||
db.field_metadata)
|
||||
self.library_view.set_current_row(0)
|
||||
# Run a garbage collection now so that it does not freeze the
|
||||
# interface later
|
||||
gc.collect()
|
||||
|
101
src/calibre/srv/embedded.py
Normal file
101
src/calibre/srv/embedded.py
Normal file
@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
from threading import Thread
|
||||
|
||||
from calibre import as_unicode
|
||||
from calibre.constants import cache_dir
|
||||
from calibre.srv.bonjour import BonJour
|
||||
from calibre.srv.handler import Handler
|
||||
from calibre.srv.http_response import create_http_handler
|
||||
from calibre.srv.loop import ServerLoop
|
||||
from calibre.srv.utils import RotatingLog
|
||||
|
||||
|
||||
def log_paths():
|
||||
return os.path.join(cache_dir(), 'server-log.txt'), os.path.join(
|
||||
cache_dir(), 'server-access-log.txt'
|
||||
)
|
||||
|
||||
|
||||
class Server(object):
|
||||
|
||||
loop = current_thread = exception = None
|
||||
state_callback = start_failure_callback = None
|
||||
|
||||
def __init__(self, opts):
|
||||
lp, lap = log_paths()
|
||||
log_size = opts.max_log_size * 1024 * 1024
|
||||
log = RotatingLog(lp, max_size=log_size)
|
||||
access_log = RotatingLog(lap, max_size=log_size)
|
||||
self.handler = Handler(libraries, opts)
|
||||
plugins = self.plugins = []
|
||||
if opts.use_bonjour:
|
||||
plugins.append(BonJour())
|
||||
self.opts = opts
|
||||
self.log, self.access_log = log, access_log
|
||||
self.handler.set_log(self.log)
|
||||
_df = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
||||
if _df and os.path.exists(_df):
|
||||
from calibre.utils.rapydscript import compile_srv
|
||||
compile_srv()
|
||||
|
||||
def start(self):
|
||||
if self.current_thread is None:
|
||||
try:
|
||||
self.loop = ServerLoop(
|
||||
create_http_handler(self.handler.dispatch),
|
||||
opts=self.opts,
|
||||
log=self.log,
|
||||
access_log=self.access_log,
|
||||
plugins=self.plugins
|
||||
)
|
||||
self.loop.initialize_socket()
|
||||
except Exception as e:
|
||||
self.loop = None
|
||||
if self.start_failure_callback is not None:
|
||||
try:
|
||||
self.start_failure_callback(as_unicode(e))
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
self.handler.set_jobs_manager(self.loop.jobs_manager)
|
||||
self.current_thread = t = Thread(
|
||||
name='EmbeddedServer', target=self.serve_forever
|
||||
)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def serve_forever(self):
|
||||
if self.state_callback is not None:
|
||||
try:
|
||||
self.state_callback(True)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.loop.serve_forever()
|
||||
except BaseException as e:
|
||||
self.exception = e
|
||||
if self.state_callback is not None:
|
||||
try:
|
||||
self.state_callback(False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
if self.loop is not None:
|
||||
self.loop.stop()
|
||||
self.loop = None
|
||||
|
||||
def exit(self):
|
||||
if self.current_thread is not None:
|
||||
self.stop()
|
||||
self.current_thread.join()
|
||||
self.current_thread = None
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
return self.current_thread is not None and self.current_thread.is_alive()
|
@ -6,79 +6,17 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import os, json
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
from importlib import import_module
|
||||
from threading import Lock
|
||||
|
||||
from calibre import force_unicode, filesystem_encoding
|
||||
from calibre.db.cache import Cache
|
||||
from calibre.db.legacy import create_backend, LibraryDatabase
|
||||
from calibre.srv.auth import AuthController
|
||||
from calibre.srv.routes import Router
|
||||
from calibre.srv.users import UserManager
|
||||
from calibre.srv.library_broker import LibraryBroker
|
||||
from calibre.utils.date import utcnow
|
||||
|
||||
|
||||
def init_library(library_path):
|
||||
db = Cache(create_backend(library_path))
|
||||
db.init()
|
||||
return db
|
||||
|
||||
|
||||
class LibraryBroker(object):
|
||||
|
||||
def __init__(self, libraries):
|
||||
self.lock = Lock()
|
||||
self.lmap = {}
|
||||
seen = set()
|
||||
for i, path in enumerate(os.path.abspath(p) for p in libraries):
|
||||
if path in seen:
|
||||
continue
|
||||
seen.add(path)
|
||||
if not LibraryDatabase.exists_at(path):
|
||||
continue
|
||||
bname = library_id = force_unicode(os.path.basename(path), filesystem_encoding).replace(' ', '_')
|
||||
c = 0
|
||||
while library_id in self.lmap:
|
||||
c += 1
|
||||
library_id = bname + '%d' % c
|
||||
if i == 0:
|
||||
self.default_library = library_id
|
||||
self.lmap[library_id] = path
|
||||
self.category_caches = {lid:OrderedDict() for lid in self.lmap}
|
||||
self.search_caches = {lid:OrderedDict() for lid in self.lmap}
|
||||
self.tag_browser_caches = {lid:OrderedDict() for lid in self.lmap}
|
||||
|
||||
def get(self, library_id=None):
|
||||
with self.lock:
|
||||
library_id = library_id or self.default_library
|
||||
ans = self.lmap.get(library_id)
|
||||
if ans is None:
|
||||
return
|
||||
if not callable(getattr(ans, 'init', None)):
|
||||
try:
|
||||
self.lmap[library_id] = ans = init_library(ans)
|
||||
ans.server_library_id = library_id
|
||||
except Exception:
|
||||
self.lmap[library_id] = ans = None
|
||||
raise
|
||||
return ans
|
||||
|
||||
def close(self):
|
||||
for db in self.lmap.itervalues():
|
||||
getattr(db, 'close', lambda : None)()
|
||||
self.lmap = {}
|
||||
|
||||
@property
|
||||
def library_map(self):
|
||||
def lpath(x):
|
||||
if hasattr(x, 'rpartition'):
|
||||
return x
|
||||
return x.backend.library_path
|
||||
return {k:os.path.basename(lpath(v)) for k, v in self.lmap.iteritems()}
|
||||
|
||||
|
||||
class Context(object):
|
||||
|
||||
log = None
|
||||
@ -209,4 +147,3 @@ class Handler(object):
|
||||
|
||||
def close(self):
|
||||
self.router.ctx.library_broker.close()
|
||||
|
||||
|
180
src/calibre/srv/library_broker.py
Normal file
180
src/calibre/srv/library_broker.py
Normal file
@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
from collections import OrderedDict, defaultdict
|
||||
from threading import Lock
|
||||
|
||||
from calibre import filesystem_encoding
|
||||
from calibre.db.cache import Cache
|
||||
from calibre.db.legacy import LibraryDatabase, create_backend, set_global_state
|
||||
from calibre.utils.filenames import samefile
|
||||
from calibre.utils.monotonic import monotonic
|
||||
|
||||
|
||||
def init_library(library_path, is_default_library):
|
||||
db = Cache(
|
||||
create_backend(
|
||||
library_path, load_user_formatter_functions=is_default_library
|
||||
)
|
||||
)
|
||||
db.init()
|
||||
return db
|
||||
|
||||
|
||||
def make_library_id_unique(library_id, existing):
|
||||
bname = library_id
|
||||
c = 0
|
||||
while library_id in existing:
|
||||
c += 1
|
||||
library_id = bname + ('%d' % c)
|
||||
return library_id
|
||||
|
||||
|
||||
def canonicalize_path(p):
|
||||
if isinstance(p, bytes):
|
||||
p = p.decode(filesystem_encoding)
|
||||
p = os.path.abspath(p).replace(os.sep, '/').rstrip('/')
|
||||
return p
|
||||
|
||||
|
||||
def library_id_from_path(path, existing):
|
||||
library_id = os.path.basename(path).replace(' ', '_')
|
||||
return make_library_id_unique(library_id, existing)
|
||||
|
||||
|
||||
class LibraryBroker(object):
|
||||
|
||||
def __init__(self, libraries):
|
||||
self.lock = Lock()
|
||||
self.lmap = {}
|
||||
seen = set()
|
||||
for i, path in enumerate(canonicalize_path(p) for p in libraries):
|
||||
if path in seen:
|
||||
continue
|
||||
seen.add(path)
|
||||
if not LibraryDatabase.exists_at(path):
|
||||
continue
|
||||
library_id = library_id_from_path(path, self.lmap)
|
||||
if i == 0:
|
||||
self.default_library = library_id
|
||||
self.lmap[library_id] = path
|
||||
self.loaded_dbs = {}
|
||||
self.category_caches, self.search_caches, self.tag_browser_caches = (
|
||||
defaultdict(OrderedDict), defaultdict(OrderedDict), defaultdict(OrderedDict))
|
||||
|
||||
def get(self, library_id=None):
|
||||
with self:
|
||||
library_id = library_id or self.default_library
|
||||
if library_id in self.loaded_dbs:
|
||||
return self.loaded_dbs[library_id]
|
||||
path = self.lmap.get(library_id)
|
||||
if path is None:
|
||||
return
|
||||
try:
|
||||
self.loaded_dbs[library_id] = ans = self.init_library(
|
||||
path, library_id == self.default_library
|
||||
)
|
||||
ans.new_api.server_library_id = library_id
|
||||
except Exception:
|
||||
self.loaded_dbs[library_id] = None
|
||||
raise
|
||||
return ans
|
||||
|
||||
def init_library(self, library_path, is_default_library):
|
||||
return init_library(library_path, is_default_library)
|
||||
|
||||
def close(self):
|
||||
with self:
|
||||
for db in self.loaded_dbs.itervalues():
|
||||
getattr(db, 'close', lambda: None)()
|
||||
self.lmap, self.loaded_dbs = {}, {}
|
||||
|
||||
@property
|
||||
def library_map(self):
|
||||
return {k: os.path.basename(v) for k, v in self.lmap.iteritems()}
|
||||
|
||||
def __enter__(self):
|
||||
self.lock.acquire()
|
||||
|
||||
def __exit__(self, *a):
|
||||
self.lock.release()
|
||||
|
||||
|
||||
EXPIRED_AGE = 300 # seconds
|
||||
|
||||
|
||||
class GuiLibraryBroker(LibraryBroker):
|
||||
|
||||
def __init__(self, db):
|
||||
from calibre.gui2 import gprefs
|
||||
stats = gprefs.get('library_usage_stats', {})
|
||||
libraries = sorted(stats, key=stats.get, reverse=True)
|
||||
self.last_used_times = defaultdict(lambda: -EXPIRED_AGE)
|
||||
self.gui_library_id = None
|
||||
LibraryBroker.__init__(self, libraries)
|
||||
self.gui_library_changed(db)
|
||||
|
||||
def init_library(self, library_path, is_default_library):
|
||||
return LibraryDatabase(library_path, is_second_db=True)
|
||||
|
||||
def get(self, library_id=None):
|
||||
try:
|
||||
return getattr(LibraryBroker.get(self, library_id), 'new_api', None)
|
||||
finally:
|
||||
self.last_used_times[library_id or self.default_library] = monotonic()
|
||||
|
||||
def prepare_for_gui_library_change(self, newloc):
|
||||
# Must be called with lock held
|
||||
for library_id, path in self.lmap.iteritems():
|
||||
db = self.loaded_dbs.get(library_id)
|
||||
if db is not None and samefile(newloc, path):
|
||||
if library_id == self.gui_library_id:
|
||||
# Have to reload db
|
||||
self.loaded_dbs.pop(library_id, None)
|
||||
return
|
||||
set_global_state(db)
|
||||
return db
|
||||
|
||||
def gui_library_changed(self, db, prune=True):
|
||||
# Must be called with lock held
|
||||
newloc = canonicalize_path(db.backend.library_path)
|
||||
for library_id, path in self.lmap.iteritems():
|
||||
if samefile(newloc, path):
|
||||
self.loaded_dbs[library_id] = db
|
||||
self.gui_library_id = library_id
|
||||
break
|
||||
else:
|
||||
library_id = self.gui_library_id = library_id_from_path(newloc, self.lmap)
|
||||
self.lmap[library_id] = newloc
|
||||
self.loaded_dbs[library_id] = db
|
||||
if prune:
|
||||
self._prune_loaded_dbs()
|
||||
|
||||
def _prune_loaded_dbs(self):
|
||||
now = monotonic()
|
||||
for library_id in tuple(self.loaded_dbs):
|
||||
if library_id != self.gui_library_id and now - self.last_used_times[library_id] > EXPIRED_AGE:
|
||||
db = self.loaded_dbs.pop(library_id)
|
||||
db.close()
|
||||
db.break_cycles()
|
||||
|
||||
def prune_loaded_dbs(self):
|
||||
with self:
|
||||
self._prune_loaded_dbs()
|
||||
|
||||
def remove_library(self, path):
|
||||
with self:
|
||||
path = canonicalize_path(path)
|
||||
for library_id, q in self.lmap.iteritems():
|
||||
if samefile(path, q):
|
||||
break
|
||||
else:
|
||||
return
|
||||
self.lmap.pop(library_id, None)
|
||||
db = self.loaded_dbs.pop(library_id)
|
||||
if db is not None:
|
||||
db.close(), db.break_cycles()
|
@ -387,9 +387,7 @@ class ServerLoop(object):
|
||||
if not self.socket:
|
||||
raise socket.error(msg)
|
||||
|
||||
def serve_forever(self):
|
||||
""" Listen for incoming connections. """
|
||||
|
||||
def initialize_socket(self):
|
||||
if self.pre_activated_socket is None:
|
||||
try:
|
||||
self.do_bind()
|
||||
@ -408,6 +406,7 @@ class ServerLoop(object):
|
||||
self.pre_activated_socket = None
|
||||
self.setup_socket()
|
||||
|
||||
def serve(self):
|
||||
self.connection_map = {}
|
||||
self.socket.listen(min(socket.SOMAXCONN, 128))
|
||||
self.bound_address = ba = self.socket.getsockname()
|
||||
@ -433,6 +432,11 @@ class ServerLoop(object):
|
||||
self.log.exception('Error in ServerLoop.tick')
|
||||
self.shutdown()
|
||||
|
||||
def serve_forever(self):
|
||||
""" Listen for incoming connections. """
|
||||
self.initialize_socket()
|
||||
self.serve()
|
||||
|
||||
def setup_socket(self):
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
Loading…
x
Reference in New Issue
Block a user