Portable: Add support for multiple libraries

This commit is contained in:
Kovid Goyal 2012-09-17 17:43:16 +05:30
parent 3c1053b765
commit bd83b0532e
7 changed files with 126 additions and 73 deletions

View File

@ -381,7 +381,6 @@ class Win32Freeze(Command, WixMixIn):
sys.exit(1) sys.exit(1)
def build_portable_installer(self): def build_portable_installer(self):
base = self.portable_base
zf = self.a(self.j('dist', 'calibre-portable-%s.zip.lz'%VERSION)) zf = self.a(self.j('dist', 'calibre-portable-%s.zip.lz'%VERSION))
usz = os.path.getsize(zf) usz = os.path.getsize(zf)
def cc(src, obj): def cc(src, obj):
@ -442,7 +441,7 @@ class Win32Freeze(Command, WixMixIn):
'/RELEASE', '/RELEASE',
'/ENTRY:wWinMainCRTStartup', '/ENTRY:wWinMainCRTStartup',
'/OUT:'+exe, self.embed_resources(exe), '/OUT:'+exe, self.embed_resources(exe),
obj, 'User32.lib', 'Shlwapi.lib'] obj, 'User32.lib']
self.run_builder(cmd) self.run_builder(cmd)
self.info('Creating portable installer') self.info('Creating portable installer')

View File

@ -8,7 +8,6 @@
#include <windows.h> #include <windows.h>
#include <Shlwapi.h>
#include <tchar.h> #include <tchar.h>
#include <wchar.h> #include <wchar.h>
#include <stdio.h> #include <stdio.h>
@ -90,7 +89,7 @@ LPTSTR get_app_dir() {
return buf3; return buf3;
} }
void launch_calibre(LPCTSTR exe, LPCTSTR config_dir, LPCTSTR library_dir) { void launch_calibre(LPCTSTR exe, LPCTSTR config_dir) {
DWORD dwFlags=0; DWORD dwFlags=0;
STARTUPINFO si; STARTUPINFO si;
PROCESS_INFORMATION pi; PROCESS_INFORMATION pi;
@ -108,13 +107,12 @@ void launch_calibre(LPCTSTR exe, LPCTSTR config_dir, LPCTSTR library_dir) {
} }
dwFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP; dwFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP;
_sntprintf_s(cmdline, BUFSIZE, _TRUNCATE, _T(" \"--with-library=%s\""), library_dir);
ZeroMemory( &si, sizeof(si) ); ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si); si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) ); ZeroMemory( &pi, sizeof(pi) );
fSuccess = CreateProcess(exe, cmdline, fSuccess = CreateProcess(exe, NULL,
NULL, // Process handle not inheritable NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable NULL, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE FALSE, // Set handle inheritance to FALSE
@ -135,45 +133,6 @@ void launch_calibre(LPCTSTR exe, LPCTSTR config_dir, LPCTSTR library_dir) {
} }
static BOOL is_dots(LPCTSTR name) {
return _tcscmp(name, _T(".")) == 0 || _tcscmp(name, _T("..")) == 0;
}
static void find_calibre_library(LPTSTR library_dir) {
TCHAR base[BUFSIZE] = {0}, buf[BUFSIZE] = {0};
WIN32_FIND_DATA fdFile;
HANDLE hFind = NULL;
_sntprintf_s(buf, BUFSIZE, _TRUNCATE, _T("%s\\metadata.db"), base);
if (PathFileExists(buf)) return; // Calibre Library/metadata.db exists, we use it
_tcscpy(base, library_dir);
PathRemoveFileSpec(base);
_sntprintf_s(buf, BUFSIZE, _TRUNCATE, _T("%s\\*"), base);
// Look for some other folder that contains a metadata.db file inside the Calibre Portable folder
if((hFind = FindFirstFileEx(buf, FindExInfoStandard, &fdFile, FindExSearchLimitToDirectories, NULL, 0))
!= INVALID_HANDLE_VALUE) {
do {
if(is_dots(fdFile.cFileName)) continue;
if(fdFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
_sntprintf_s(buf, BUFSIZE, _TRUNCATE, _T("%s\\%s\\metadata.db"), base, fdFile.cFileName);
if (PathFileExists(buf)) {
// some dir/metadata.db exists, we use it as the library
PathRemoveFileSpec(buf);
_tcscpy(library_dir, buf);
FindClose(hFind);
return;
}
}
} while(FindNextFile(hFind, &fdFile));
FindClose(hFind);
}
}
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
{ {
@ -181,26 +140,14 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine
app_dir = get_app_dir(); app_dir = get_app_dir();
config_dir = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR)); config_dir = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
library_dir = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
exe = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR)); exe = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
_sntprintf_s(config_dir, BUFSIZE, _TRUNCATE, _T("%sCalibre Settings"), app_dir); _sntprintf_s(config_dir, BUFSIZE, _TRUNCATE, _T("%sCalibre Settings"), app_dir);
_sntprintf_s(exe, BUFSIZE, _TRUNCATE, _T("%sCalibre\\calibre.exe"), app_dir); _sntprintf_s(exe, BUFSIZE, _TRUNCATE, _T("%sCalibre\\calibre.exe"), app_dir);
_sntprintf_s(library_dir, BUFSIZE, _TRUNCATE, _T("%sCalibre Library"), app_dir);
find_calibre_library(library_dir); launch_calibre(exe, config_dir);
if ( _tcscnlen(library_dir, BUFSIZE) <= 74 ) { free(app_dir); free(config_dir); free(exe);
launch_calibre(exe, config_dir, library_dir);
} else {
too_long = (LPTSTR)calloc(BUFSIZE+300, sizeof(TCHAR));
_sntprintf_s(too_long, BUFSIZE+300, _TRUNCATE,
_T("Path to Calibre Portable (%s) too long. Must be less than 59 characters."), app_dir);
show_error(too_long);
}
free(app_dir); free(config_dir); free(exe); free(library_dir);
return 0; return 0;
} }

View File

@ -177,6 +177,11 @@ def get_version():
v += '*' v += '*'
return v return v
def get_portable_base():
'Return path to the directory that contains calibre-portable.exe or None'
if isportable:
return os.path.dirname(os.path.dirname(os.environ['CALIBRE_PORTABLE_BUILD']))
def get_unicode_windows_env_var(name): def get_unicode_windows_env_var(name):
import ctypes import ctypes
name = unicode(name) name = unicode(name)

View File

@ -567,7 +567,8 @@ class FileDialog(QObject):
modal = True, modal = True,
name = '', name = '',
mode = QFileDialog.ExistingFiles, mode = QFileDialog.ExistingFiles,
default_dir='~' default_dir='~',
no_save_dir=False
): ):
QObject.__init__(self) QObject.__init__(self)
ftext = '' ftext = ''
@ -586,6 +587,9 @@ class FileDialog(QObject):
self.selected_files = None self.selected_files = None
self.fd = None self.fd = None
if no_save_dir:
initial_dir = os.path.expanduser(default_dir)
else:
initial_dir = dynamic.get(self.dialog_name, initial_dir = dynamic.get(self.dialog_name,
os.path.expanduser(default_dir)) os.path.expanduser(default_dir))
if not isinstance(initial_dir, basestring): if not isinstance(initial_dir, basestring):
@ -629,6 +633,7 @@ class FileDialog(QObject):
saved_loc = self.selected_files[0] saved_loc = self.selected_files[0]
if os.path.isfile(saved_loc): if os.path.isfile(saved_loc):
saved_loc = os.path.dirname(saved_loc) saved_loc = os.path.dirname(saved_loc)
if not no_save_dir:
dynamic[self.dialog_name] = saved_loc dynamic[self.dialog_name] = saved_loc
self.accepted = bool(self.selected_files) self.accepted = bool(self.selected_files)
@ -638,10 +643,10 @@ class FileDialog(QObject):
return tuple(self.selected_files) return tuple(self.selected_files)
def choose_dir(window, name, title, default_dir='~'): def choose_dir(window, name, title, default_dir='~', no_save_dir=False):
fd = FileDialog(title=title, filters=[], add_all_files_filter=False, fd = FileDialog(title=title, filters=[], add_all_files_filter=False,
parent=window, name=name, mode=QFileDialog.Directory, parent=window, name=name, mode=QFileDialog.Directory,
default_dir=default_dir) default_dir=default_dir, no_save_dir=no_save_dir)
dir = fd.get_files() dir = fd.get_files()
fd.setParent(None) fd.setParent(None)
if dir: if dir:

View File

@ -5,7 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os import os, posixpath
from functools import partial from functools import partial
from PyQt4.Qt import (QMenu, Qt, QInputDialog, QToolButton, QDialog, from PyQt4.Qt import (QMenu, Qt, QInputDialog, QToolButton, QDialog,
@ -13,7 +13,8 @@ from PyQt4.Qt import (QMenu, Qt, QInputDialog, QToolButton, QDialog,
QCoreApplication, pyqtSignal) QCoreApplication, pyqtSignal)
from calibre import isbytestring, sanitize_file_name_unicode from calibre import isbytestring, sanitize_file_name_unicode
from calibre.constants import filesystem_encoding, iswindows from calibre.constants import (filesystem_encoding, iswindows,
get_portable_base)
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.gui2 import (gprefs, warning_dialog, Dispatcher, error_dialog, from calibre.gui2 import (gprefs, warning_dialog, Dispatcher, error_dialog,
question_dialog, info_dialog, open_local_file, choose_dir) question_dialog, info_dialog, open_local_file, choose_dir)
@ -25,6 +26,17 @@ class LibraryUsageStats(object): # {{{
def __init__(self): def __init__(self):
self.stats = {} self.stats = {}
self.read_stats() self.read_stats()
base = get_portable_base()
if base is not None:
lp = prefs['library_path']
if lp:
# Rename the current library. Renaming of other libraries is
# handled by the switch function
q = os.path.basename(lp)
for loc in list(self.stats.iterkeys()):
bn = posixpath.basename(loc)
if bn.lower() == q.lower():
self.rename(loc, lp)
def read_stats(self): def read_stats(self):
stats = gprefs.get('library_usage_stats', {}) stats = gprefs.get('library_usage_stats', {})
@ -417,6 +429,18 @@ class ChooseLibraryAction(InterfaceAction):
finally: finally:
self.gui.status_bar.clear_message() self.gui.status_bar.clear_message()
def look_for_portable_lib(self, db, location):
base = get_portable_base()
if base is None:
return False, None
loc = location.replace('/', os.sep)
candidate = os.path.join(base, os.path.basename(loc))
if db.exists_at(candidate):
newloc = candidate.replace(os.sep, '/')
self.stats.rename(location, newloc)
return True, newloc
return False, None
def switch_requested(self, location): def switch_requested(self, location):
if not self.change_library_allowed(): if not self.change_library_allowed():
return return
@ -425,6 +449,12 @@ class ChooseLibraryAction(InterfaceAction):
self.view_state_map[current_lib] = self.preserve_state_on_switch.state self.view_state_map[current_lib] = self.preserve_state_on_switch.state
loc = location.replace('/', os.sep) loc = location.replace('/', os.sep)
exists = db.exists_at(loc) exists = db.exists_at(loc)
if not exists:
exists, new_location = self.look_for_portable_lib(db, location)
if exists:
location = new_location
loc = location.replace('/', os.sep)
if not exists: if not exists:
d = MovedDialog(self.stats, location, self.gui) d = MovedDialog(self.stats, location, self.gui)
ret = d.exec_() ret = d.exec_()

View File

@ -11,8 +11,9 @@ from PyQt4.Qt import QDialog
from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog
from calibre.gui2 import error_dialog, choose_dir from calibre.gui2 import error_dialog, choose_dir
from calibre.constants import filesystem_encoding, iswindows from calibre.constants import (filesystem_encoding, iswindows,
from calibre import isbytestring, patheq get_portable_base)
from calibre import isbytestring, patheq, force_unicode
from calibre.gui2.wizard import move_library from calibre.gui2.wizard import move_library
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
@ -39,18 +40,45 @@ class ChooseLibrary(QDialog, Ui_Dialog):
self.copy_structure.setEnabled(to_what) self.copy_structure.setEnabled(to_what)
def choose_loc(self, *args): def choose_loc(self, *args):
base = get_portable_base()
if base is None:
loc = choose_dir(self, 'choose library location', loc = choose_dir(self, 'choose library location',
_('Choose location for calibre library')) _('Choose location for calibre library'))
else:
name = force_unicode('choose library loc at' + base,
filesystem_encoding)
loc = choose_dir(self, name,
_('Choose location for calibre library'), default_dir=base,
no_save_dir=True)
if loc is not None: if loc is not None:
self.location.setText(loc) self.location.setText(loc)
def check_action(self, ac, loc): def check_action(self, ac, loc):
exists = self.db.exists_at(loc) exists = self.db.exists_at(loc)
base = get_portable_base()
if patheq(loc, self.db.library_path): if patheq(loc, self.db.library_path):
error_dialog(self, _('Same as current'), error_dialog(self, _('Same as current'),
_('The location %s contains the current calibre' _('The location %s contains the current calibre'
' library')%loc, show=True) ' library')%loc, show=True)
return False return False
if base is not None and ac in ('new', 'move'):
abase = os.path.normcase(os.path.abspath(base))
cal = os.path.normcase(os.path.abspath(os.path.join(abase,
'Calibre')))
aloc = os.path.normcase(os.path.abspath(loc))
if (aloc.startswith(cal+os.sep) or aloc == cal):
error_dialog(self, _('Bad location'),
_('You should not create a library inside the Calibre'
' folder as this folder is automatically deleted during upgrades.'),
show=True)
return False
if aloc.startswith(abase) and os.path.dirname(aloc) != abase:
error_dialog(self, _('Bad location'),
_('You can only create libraries inside %s at the top '
'level, not in sub-folders')%base, show=True)
return False
empty = not os.listdir(loc) empty = not os.listdir(loc)
if ac == 'existing' and not exists: if ac == 'existing' and not exists:
error_dialog(self, _('No existing library found'), error_dialog(self, _('No existing library found'),

View File

@ -9,7 +9,7 @@ from PyQt4.Qt import (QCoreApplication, QIcon, QObject, QTimer,
from calibre import prints, plugins, force_unicode from calibre import prints, plugins, force_unicode
from calibre.constants import (iswindows, __appname__, isosx, DEBUG, islinux, from calibre.constants import (iswindows, __appname__, isosx, DEBUG, islinux,
filesystem_encoding) filesystem_encoding, get_portable_base)
from calibre.utils.ipc import gui_socket_address, RC from calibre.utils.ipc import gui_socket_address, RC
from calibre.gui2 import (ORG_NAME, APP_UID, initialize_file_icon_provider, from calibre.gui2 import (ORG_NAME, APP_UID, initialize_file_icon_provider,
Application, choose_dir, error_dialog, question_dialog, gprefs) Application, choose_dir, error_dialog, question_dialog, gprefs)
@ -21,6 +21,9 @@ from calibre.library.sqlite import sqlite, DatabaseException
if iswindows: if iswindows:
winutil = plugins['winutil'][0] winutil = plugins['winutil'][0]
class AbortInit(Exception):
pass
def option_parser(): def option_parser():
parser = _option_parser('''\ parser = _option_parser('''\
%prog [opts] [path_to_ebook] %prog [opts] [path_to_ebook]
@ -46,10 +49,43 @@ path_to_ebook to the database.
'will be silently aborted, so use with care.')) 'will be silently aborted, so use with care.'))
return parser return parser
def find_portable_library():
base = get_portable_base()
if base is None: return
import glob
candidates = [os.path.basename(os.path.dirname(x)) for x in glob.glob(
os.path.join(base, u'*%smetadata.db'%os.sep))]
if not candidates:
candidates = [u'Calibre Library']
lp = prefs['library_path']
if not lp:
lib = os.path.join(base, candidates[0])
else:
lib = None
q = os.path.basename(lp)
for c in candidates:
c = c
if c.lower() == q.lower():
lib = os.path.join(base, c)
break
if lib is None:
lib = os.path.join(base, candidates[0])
if len(lib) > 74:
error_dialog(None, _('Path too long'),
_("Path to Calibre Portable (%s) "
'too long. Must be less than 59 characters.')%base, show=True)
raise AbortInit()
prefs.set('library_path', lib)
if not os.path.exists(lib):
os.mkdir(lib)
def init_qt(args): def init_qt(args):
from calibre.gui2.ui import Main from calibre.gui2.ui import Main
parser = option_parser() parser = option_parser()
opts, args = parser.parse_args(args) opts, args = parser.parse_args(args)
find_portable_library()
if opts.with_library is not None: if opts.with_library is not None:
if not os.path.exists(opts.with_library): if not os.path.exists(opts.with_library):
os.makedirs(opts.with_library) os.makedirs(opts.with_library)
@ -360,7 +396,10 @@ def main(args=sys.argv):
gui_debug = args[1] gui_debug = args[1]
args = ['calibre'] args = ['calibre']
try:
app, opts, args, actions = init_qt(args) app, opts, args, actions = init_qt(args)
except AbortInit:
return 1
from calibre.utils.lock import singleinstance from calibre.utils.lock import singleinstance
from multiprocessing.connection import Listener from multiprocessing.connection import Listener
si = singleinstance('calibre GUI') si = singleinstance('calibre GUI')