%PDF- %PDF-
Direktori : /lib/calibre/calibre/utils/ |
Current File : //lib/calibre/calibre/utils/exim.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net> import os, json, struct, hashlib, sys, errno, tempfile, time, shutil, uuid from collections import Counter from calibre import prints from calibre.constants import config_dir, iswindows, filesystem_encoding from calibre.utils.config_base import prefs, StringConfig, create_global_prefs from calibre.utils.config import JSONConfig from calibre.utils.filenames import samefile from polyglot.builtins import iteritems, error_message from polyglot.binary import as_hex_unicode # Export {{{ def send_file(from_obj, to_obj, chunksize=1<<20): m = hashlib.sha1() while True: raw = from_obj.read(chunksize) if not raw: break m.update(raw) to_obj.write(raw) return str(m.hexdigest()) class FileDest: def __init__(self, key, exporter, mtime=None): self.exporter, self.key = exporter, key self.hasher = hashlib.sha1() self.start_pos = exporter.f.tell() self._discard = False self.mtime = None def discard(self): self._discard = True def ensure_space(self, size): if size > 0: self.exporter.ensure_space(size) self.start_pos = self.exporter.f.tell() def write(self, data): self.hasher.update(data) self.exporter.f.write(data) def flush(self): pass def close(self): if not self._discard: size = self.exporter.f.tell() - self.start_pos digest = str(self.hasher.hexdigest()) self.exporter.file_metadata[self.key] = (len(self.exporter.parts), self.start_pos, size, digest, self.mtime) del self.exporter, self.hasher def __enter__(self): return self def __exit__(self, *args): self.close() class Exporter: VERSION = 0 TAIL_FMT = b'!II?' # part_num, version, is_last MDATA_SZ_FMT = b'!Q' EXT = '.calibre-data' def __init__(self, path_to_export_dir, part_size=(1 << 30)): self.part_size = part_size self.base = os.path.abspath(path_to_export_dir) self.parts = [] self.new_part() self.file_metadata = {} self.metadata = {'file_metadata': self.file_metadata} def set_metadata(self, key, val): if key in self.metadata: raise KeyError('The metadata already contains the key: %s' % key) self.metadata[key] = val @property def f(self): return self.parts[-1] def new_part(self): self.parts.append(open(os.path.join( self.base, f'part-{len(self.parts) + 1:04d}{self.EXT}'), 'wb')) def commit_part(self, is_last=False): self.f.write(struct.pack(self.TAIL_FMT, len(self.parts), self.VERSION, is_last)) self.f.close() self.parts[-1] = self.f.name def ensure_space(self, size): try: if size + self.f.tell() < self.part_size: return except AttributeError: raise RuntimeError('This exporter has already been committed, cannot add to it') self.commit_part() self.new_part() def commit(self): raw = json.dumps(self.metadata, ensure_ascii=False) if not isinstance(raw, bytes): raw = raw.encode('utf-8') self.ensure_space(len(raw)) self.f.write(raw) self.f.write(struct.pack(self.MDATA_SZ_FMT, len(raw))) self.commit_part(is_last=True) def add_file(self, fileobj, key): fileobj.seek(0, os.SEEK_END) size = fileobj.tell() fileobj.seek(0) self.ensure_space(size) pos = self.f.tell() digest = send_file(fileobj, self.f) size = self.f.tell() - pos mtime = os.fstat(fileobj.fileno()).st_mtime self.file_metadata[key] = (len(self.parts), pos, size, digest, mtime) def start_file(self, key, mtime=None): return FileDest(key, self, mtime=mtime) def export_dir(self, path, dir_key): pkey = as_hex_unicode(dir_key) self.metadata[dir_key] = files = [] for dirpath, dirnames, filenames in os.walk(path): for fname in filenames: fpath = os.path.join(dirpath, fname) rpath = os.path.relpath(fpath, path).replace(os.sep, '/') key = f'{pkey}:{rpath}' try: with lopen(fpath, 'rb') as f: self.add_file(f, key) except OSError: if not iswindows: raise time.sleep(1) with lopen(fpath, 'rb') as f: self.add_file(f, key) files.append((key, rpath)) def all_known_libraries(): from calibre.gui2 import gprefs lus = gprefs.get('library_usage_stats', {}) paths = set(lus) if prefs['library_path']: paths.add(prefs['library_path']) added = {} for path in paths: mdb = os.path.join(path, 'metadata.db') if os.path.exists(mdb): for c in added: if samefile(mdb, os.path.join(c, 'metadata.db')): break else: added[path] = lus.get(path, 1) return added def export(destdir, library_paths=None, dbmap=None, progress1=None, progress2=None, abort=None): from calibre.db.cache import Cache from calibre.db.backend import DB if library_paths is None: library_paths = all_known_libraries() dbmap = dbmap or {} dbmap = {os.path.normcase(os.path.abspath(k)):v for k, v in iteritems(dbmap)} exporter = Exporter(destdir) exporter.metadata['libraries'] = libraries = {} total = len(library_paths) + 1 for i, (lpath, count) in enumerate(iteritems(library_paths)): if abort is not None and abort.is_set(): return if progress1 is not None: progress1(lpath, i, total) key = os.path.normcase(os.path.abspath(lpath)) db, closedb = dbmap.get(lpath), False if db is None: db = Cache(DB(lpath, load_user_formatter_functions=False)) db.init() closedb = True else: db = db.new_api db.export_library(key, exporter, progress=progress2, abort=abort) if closedb: db.close() libraries[key] = count if progress1 is not None: progress1(_('Settings and plugins'), total-1, total) if abort is not None and abort.is_set(): return exporter.export_dir(config_dir, 'config_dir') exporter.commit() if progress1 is not None: progress1(_('Completed'), total, total) # }}} # Import {{{ class FileSource: def __init__(self, f, size, digest, description, mtime, importer): self.f, self.size, self.digest, self.description = f, size, digest, description self.mtime = mtime self.end = f.tell() + size self.hasher = hashlib.sha1() self.importer = importer def read(self, size=None): if size is not None and size < 1: return b'' left = self.end - self.f.tell() amt = min(left, size or left) if amt < 1: return b'' ans = self.f.read(amt) self.hasher.update(ans) return ans def close(self): if self.hasher.hexdigest() != self.digest: self.importer.corrupted_files.append(self.description) self.hasher = self.f = None class Importer: def __init__(self, path_to_export_dir): self.corrupted_files = [] part_map = {} tail_size = struct.calcsize(Exporter.TAIL_FMT) for name in os.listdir(path_to_export_dir): if name.lower().endswith(Exporter.EXT): path = os.path.join(path_to_export_dir, name) with open(path, 'rb') as f: f.seek(-tail_size, os.SEEK_END) raw = f.read() if len(raw) != tail_size: raise ValueError('The exported data in %s is not valid, tail too small' % name) part_num, version, is_last = struct.unpack(Exporter.TAIL_FMT, raw) if version > Exporter.VERSION: raise ValueError('The exported data in %s is not valid,' ' version (%d) is higher than maximum supported version.' ' You might need to upgrade calibre first.' % (name, version)) part_map[part_num] = path, is_last nums = sorted(part_map) if not nums: raise ValueError('No exported data found in: %s' % path_to_export_dir) if nums[0] != 1: raise ValueError('The first part of this exported data set is missing') if not part_map[nums[-1]][1]: raise ValueError('The last part of this exported data set is missing') if len(nums) != nums[-1]: raise ValueError('There are some parts of the exported data set missing') self.part_map = {num:path for num, (path, is_last) in iteritems(part_map)} msf = struct.calcsize(Exporter.MDATA_SZ_FMT) offset = tail_size + msf with self.part(nums[-1]) as f: f.seek(-offset, os.SEEK_END) sz, = struct.unpack(Exporter.MDATA_SZ_FMT, f.read(msf)) f.seek(- sz - offset, os.SEEK_END) self.metadata = json.loads(f.read(sz)) self.file_metadata = self.metadata['file_metadata'] def part(self, num): return lopen(self.part_map[num], 'rb') def start_file(self, key, description): partnum, pos, size, digest, mtime = self.file_metadata[key] f = self.part(partnum) f.seek(pos) return FileSource(f, size, digest, description, mtime, self) def export_config(self, base_dir, library_usage_stats): for key, relpath in self.metadata['config_dir']: f = self.start_file(key, relpath) path = os.path.join(base_dir, relpath.replace('/', os.sep)) try: with lopen(path, 'wb') as dest: shutil.copyfileobj(f, dest) except OSError: os.makedirs(os.path.dirname(path)) with lopen(path, 'wb') as dest: shutil.copyfileobj(f, dest) f.close() gpath = os.path.join(base_dir, 'global.py') try: with lopen(gpath, 'rb') as f: raw = f.read() except OSError: raw = b'' try: lpath = library_usage_stats.most_common(1)[0][0] except Exception: lpath = None c = create_global_prefs(StringConfig(raw, 'calibre wide preferences')) c.set('installation_uuid', str(uuid.uuid4())) c.set('library_path', lpath) raw = c.src if not isinstance(raw, bytes): raw = raw.encode('utf-8') with lopen(gpath, 'wb') as f: f.write(raw) gprefs = JSONConfig('gui', base_path=base_dir) gprefs['library_usage_stats'] = dict(library_usage_stats) def import_data(importer, library_path_map, config_location=None, progress1=None, progress2=None, abort=None): from calibre.db.cache import import_library config_location = config_location or config_dir config_location = os.path.abspath(os.path.realpath(config_location)) total = len(library_path_map) + 1 library_usage_stats = Counter() for i, (library_key, dest) in enumerate(iteritems(library_path_map)): if abort is not None and abort.is_set(): return if isinstance(dest, bytes): dest = dest.decode(filesystem_encoding) if progress1 is not None: progress1(dest, i, total) try: os.makedirs(dest) except OSError as err: if err.errno != errno.EEXIST: raise if not os.path.isdir(dest): raise ValueError('%s is not a directory' % dest) import_library(library_key, importer, dest, progress=progress2, abort=abort).close() stats_key = os.path.abspath(dest).replace(os.sep, '/') library_usage_stats[stats_key] = importer.metadata['libraries'].get(library_key, 1) if progress1 is not None: progress1(_('Settings and plugins'), total - 1, total) if abort is not None and abort.is_set(): return base_dir = tempfile.mkdtemp(dir=os.path.dirname(config_location)) importer.export_config(base_dir, library_usage_stats) if os.path.lexists(config_location): if os.path.islink(config_location) or os.path.isfile(config_location): os.remove(config_location) else: shutil.rmtree(config_location, ignore_errors=True) if os.path.exists(config_location): try: shutil.rmtree(config_location) except OSError: if not iswindows: raise time.sleep(1) shutil.rmtree(config_location) try: os.rename(base_dir, config_location) except OSError: time.sleep(2) os.rename(base_dir, config_location) from calibre.gui2 import gprefs gprefs.refresh() if progress1 is not None: progress1(_('Completed'), total, total) def test_import(export_dir='/t/ex', import_dir='/t/imp'): importer = Importer(export_dir) if os.path.exists(import_dir): shutil.rmtree(import_dir) os.mkdir(import_dir) import_data(importer, {k:os.path.join(import_dir, os.path.basename(k)) for k in importer.metadata['libraries'] if 'largelib' not in k}, config_location=os.path.join(import_dir, 'calibre-config'), progress1=print, progress2=print) def cli_report(*args, **kw): try: prints(*args, **kw) except OSError: pass def input_unicode(prompt): ans = input(prompt) if isinstance(ans, bytes): ans = ans.decode(sys.stdin.encoding) return ans def run_exporter(export_dir=None, args=None): if args: if len(args) < 2: raise SystemExit('You must specify the export folder and libraries to export') export_dir = args[0] if not os.path.exists(export_dir): os.makedirs(export_dir) if os.listdir(export_dir): raise SystemExit('%s is not empty' % export_dir) all_libraries = {os.path.normcase(os.path.abspath(path)):lus for path, lus in iteritems(all_known_libraries())} if 'all' in args[1:]: libraries = set(all_libraries) else: libraries = {os.path.normcase(os.path.abspath(os.path.expanduser(path))) for path in args[1:]} if libraries - set(all_libraries): raise SystemExit('Unknown library: ' + tuple(libraries - all_libraries)[0]) libraries = {p: all_libraries[p] for p in libraries} print('Exporting libraries:', ', '.join(sorted(libraries)), 'to:', export_dir) export(export_dir, progress1=cli_report, progress2=cli_report, library_paths=libraries) return export_dir = export_dir or input_unicode( 'Enter path to an empty folder (all exported data will be saved inside it): ').rstrip('\r') if not os.path.exists(export_dir): os.makedirs(export_dir) if not os.path.isdir(export_dir): raise SystemExit('%s is not a folder' % export_dir) if os.listdir(export_dir): raise SystemExit('%s is not empty' % export_dir) library_paths = {} for lpath, lus in iteritems(all_known_libraries()): if input_unicode('Export the library %s [y/n]: ' % lpath).strip().lower() == 'y': library_paths[lpath] = lus if library_paths: export(export_dir, progress1=cli_report, progress2=cli_report, library_paths=library_paths) else: raise SystemExit('No libraries selected for export') def run_importer(): export_dir = input_unicode('Enter path to folder containing previously exported data: ').rstrip('\r') if not os.path.isdir(export_dir): raise SystemExit('%s is not a folder' % export_dir) try: importer = Importer(export_dir) except ValueError as err: raise SystemExit(error_message(err)) import_dir = input_unicode('Enter path to an empty folder (all libraries will be created inside this folder): ').rstrip('\r') if not os.path.exists(import_dir): os.makedirs(import_dir) if not os.path.isdir(import_dir): raise SystemExit('%s is not a folder' % import_dir) if os.listdir(import_dir): raise SystemExit('%s is not empty' % import_dir) import_data(importer, { k:os.path.join(import_dir, os.path.basename(k)) for k in importer.metadata['libraries']}, progress1=cli_report, progress2=cli_report) # }}} if __name__ == '__main__': export(sys.argv[-1], progress1=print, progress2=print)