def cdb_add_book(ctx, rd, job_id, add_duplicates, filename, library_id): ''' Add a file as a new book. The file contents must be in the body of the request. The response will also have the title/authors/languages read from the metadata of the file/filename. It will contain a `book_id` field specifying the id of the newly added book, or if add_duplicates is not specified and a duplicate was found, no book_id will be present. It will also return the value of `job_id` as the `id` field and `filename` as the `filename` field. ''' db = get_db(ctx, rd, library_id) if ctx.restriction_for(rd, db): raise HTTPForbidden('Cannot use the add book interface with a user who has per library restrictions') if not filename: raise HTTPBadRequest('An empty filename is not allowed') sfilename = sanitize_file_name(filename) fmt = os.path.splitext(sfilename)[1] fmt = fmt[1:] if fmt else None if not fmt: raise HTTPBadRequest('An filename with no extension is not allowed') if isinstance(rd.request_body_file, BytesIO): raise HTTPBadRequest('A request body containing the file data must be specified') add_duplicates = add_duplicates in ('y', '1') path = os.path.join(rd.tdir, sfilename) rd.request_body_file.name = path rd.request_body_file.seek(0) mi = get_metadata(rd.request_body_file, stream_type=fmt, use_libprs_metadata=True) rd.request_body_file.seek(0) ids, duplicates = db.add_books([(mi, {fmt: rd.request_body_file})], add_duplicates=add_duplicates) ans = {'title': mi.title, 'authors': mi.authors, 'languages': mi.languages, 'filename': filename, 'id': job_id} if ids: ans['book_id'] = ids[0] ctx.notify_changes(db.backend.library_path, books_added(ids)) return ans
def ascii_filename(orig, substitute='_'): ans = [] orig = ascii_text(orig).replace('?', '_') for x in orig: if ord(x) < 32: x = substitute ans.append(x) return sanitize_file_name(''.join(ans), substitute=substitute)
def book_filename(rd, book_id, mi, fmt, as_encoded_unicode=False): au = authors_to_string(mi.authors or [_('Unknown')]) title = mi.title or _('Unknown') ext = (fmt or '').lower() if ext == 'kepub' and 'Kobo Touch' in rd.inheaders.get('User-Agent', ''): ext = 'kepub.epub' fname = '%s - %s_%s.%s' % (title[:30], au[:30], book_id, ext) if as_encoded_unicode: # See https://tools.ietf.org/html/rfc6266 fname = sanitize_file_name(fname).encode('utf-8') fname = unicode_type(quote(fname)) else: fname = ascii_filename(fname).replace('"', '_') return fname
def name_is_ok(name, show_error): if not name or not name.strip(): return show_error('') and False ext = name.rpartition('.')[-1] if not ext or ext == name: return show_error(_('The file name must have an extension')) and False norm = name.replace('\\', '/') parts = name.split('/') for x in parts: if sanitize_file_name(x) != x: return show_error(_('The file name contains invalid characters')) and False if current_container().has_name(norm): return show_error(_('This file name already exists in the book')) and False show_error('') return True
def data(self): fpath = self.file_name.text().strip() head, tail = os.path.split(fpath) tail = sanitize_file_name(tail) fpath = tail if head: fpath = os.path.join(head, tail) ans = { 'output': fpath, 'paper_size': self.paper_size.currentText().lower(), 'page_numbers':self.pnum.isChecked(), 'show_file':self.show_file.isChecked(), } for edge in 'left top right bottom'.split(): ans['margin_' + edge] = getattr(self, '%s_margin' % edge).value() return ans
def rename_requested(self, name, location): LibraryDatabase = db_class() loc = location.replace('/', os.sep) base = os.path.dirname(loc) old_name = name.replace('&&', '&') newname, ok = QInputDialog.getText(self.gui, _('Rename') + ' ' + old_name, '<p>'+_( 'Choose a new name for the library <b>%s</b>. ')%name + '<p>'+_( 'Note that the actual library folder will be renamed.'), text=old_name) newname = sanitize_file_name(unicode_type(newname)) if not ok or not newname or newname == old_name: return newloc = os.path.join(base, newname) if os.path.exists(newloc): return error_dialog(self.gui, _('Already exists'), _('The folder %s already exists. Delete it first.') % newloc, show=True) if (iswindows and len(newloc) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT): return error_dialog(self.gui, _('Too long'), _('Path to library too long. Must be less than' ' %d characters.')%LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT, show=True) if not os.path.exists(loc): error_dialog(self.gui, _('Not found'), _('Cannot rename as no library was found at %s. ' '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: import traceback det_msg = 'Location: %r New Location: %r\n%s'%(loc, newloc, traceback.format_exc()) error_dialog(self.gui, _('Rename failed'), _('Failed to rename the library at %s. ' 'The most common cause for this is if one of the files' ' in the library is open in another program.') % loc, det_msg=det_msg, show=True) return self.stats.rename(location, newloc) self.build_menus() self.gui.iactions['Copy To Library'].build_menus()
def catalog_generated(self, job): if job.result: # Problems during catalog generation # jobs.results is a list - the first entry is the intended title for the dialog # Subsequent strings are error messages dialog_title = job.result.pop(0) if re.search('warning', job.result[0].lower()): msg = _("Catalog generation complete, with warnings.") warning_dialog(self.gui, dialog_title, msg, det_msg='\n'.join(job.result), show=True) else: job.result.append("Catalog generation terminated.") error_dialog(self.gui, dialog_title,'\n'.join(job.result),show=True) return if job.failed: return self.gui.job_exception(job) if dynamic.get('catalog_add_to_library', True): id = self.gui.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title) self.gui.library_view.model().beginResetModel(), self.gui.library_view.model().endResetModel() if job.catalog_sync: sync = dynamic.get('catalogs_to_be_synced', set([])) sync.add(id) dynamic.set('catalogs_to_be_synced', sync) self.gui.status_bar.show_message(_('Catalog generated.'), 3000) self.gui.sync_catalogs() if not dynamic.get('catalog_add_to_library', True) or job.fmt not in {'EPUB','MOBI', 'AZW3'}: export_dir = choose_dir(self.gui, _('Export Catalog Directory'), _('Select destination for %(title)s.%(fmt)s') % dict( title=job.catalog_title, fmt=job.fmt.lower())) if export_dir: destination = os.path.join(export_dir, '%s.%s' % ( sanitize_file_name(job.catalog_title), job.fmt.lower())) try: shutil.copyfile(job.catalog_file_path, destination) except EnvironmentError as err: if getattr(err, 'errno', None) == errno.EACCES: # Permission denied import traceback error_dialog(self.gui, _('Permission denied'), _('Could not open %s. Is it being used by another' ' program?')%destination, det_msg=traceback.format_exc(), show=True) return raise
def prepare_addable_books(self, paths): """ Given a list of paths, returns another list of paths. These paths point to addable versions of the books. If there is an error preparing a book, then instead of a path, the position in the returned list for that book should be a three tuple: (original_path, the exception instance, traceback) Modeled on calibre.devices.mtp.driver:prepare_addable_books() #304 """ from calibre import sanitize_file_name from calibre.ptempfile import PersistentTemporaryDirectory self._log_location() tdir = PersistentTemporaryDirectory("_prep_gr") ans = [] for path in paths: if not self.ios.exists("/".join([self.documents_folder, path])): ans.append((path, "File not found", "File not found")) continue base = tdir if iswindows: from calibre.utils.filenames import shorten_components_to plen = len(base) bfn = path.split("/")[-1] dest = "".join(shorten_components_to(245 - plen, [bfn])) else: dest = path out_path = os.path.normpath(os.path.join(base, sanitize_file_name(dest))) with open(out_path, "wb") as out: try: self.get_file(path, out) except Exception as e: import traceback ans.append((dest, e, traceback.format_exc())) else: ans.append(out.name) return ans
def replace_file(container, name, path, basename, force_mt=None): dirname, base = name.rpartition('/')[0::2] nname = sanitize_file_name(basename) if dirname: nname = dirname + '/' + nname with open(path, 'rb') as src: if name != nname: count = 0 b, e = nname.rpartition('.')[0::2] while container.exists(nname): count += 1 nname = b + ('_%d.%s' % (count, e)) rename_files(container, {name:nname}) mt = force_mt or container.guess_type(nname) container.mime_map[nname] = mt for itemid, q in iteritems(container.manifest_id_map): if q == nname: for item in container.opf_xpath('//opf:manifest/opf:item[@href and @id="%s"]' % itemid): item.set('media-type', mt) container.dirty(container.opf_name) with container.open(nname, 'wb') as dest: shutil.copyfileobj(src, dest)
def name_to_path(name): return os.path.join(config_dir, sanitize_file_name(name)+'.py')
def test_identify_plugin(name, tests, modify_plugin=lambda plugin:None, # {{{ fail_missing_meta=True): ''' :param name: Plugin name :param tests: List of 2-tuples. Each two tuple is of the form (args, test_funcs). args is a dict of keyword arguments to pass to the identify method. test_funcs are callables that accept a Metadata object and return True iff the object passes the test. ''' plugin = None for x in all_metadata_plugins(): if x.name == name and 'identify' in x.capabilities: plugin = x break modify_plugin(plugin) prints('Testing the identify function of', plugin.name) prints('Using extra headers:', plugin.browser.addheaders) tdir, lf, log, abort = init_test(plugin.name) prints('Log saved to', lf) times = [] for kwargs, test_funcs in tests: log('') log('#'*80) log('### Running test with:', kwargs) log('#'*80) prints('Running test with:', kwargs) rq = Queue() args = (log, rq, abort) start_time = time.time() plugin.running_a_test = True try: err = plugin.identify(*args, **kwargs) finally: plugin.running_a_test = False total_time = time.time() - start_time times.append(total_time) if err is not None: prints('identify returned an error for args', args) prints(err) break results = [] while True: try: results.append(rq.get_nowait()) except Empty: break prints('Found', len(results), 'matches:', end=' ') prints('Smaller relevance means better match') results.sort(key=plugin.identify_results_keygen( title=kwargs.get('title', None), authors=kwargs.get('authors', None), identifiers=kwargs.get('identifiers', {}))) for i, mi in enumerate(results): prints('*'*30, 'Relevance:', i, '*'*30) if mi.rating: mi.rating *= 2 prints(mi) prints('\nCached cover URL :', plugin.get_cached_cover_url(mi.identifiers)) prints('*'*75, '\n\n') possibles = [] for mi in results: test_failed = False for tfunc in test_funcs: if not tfunc(mi): test_failed = True break if not test_failed: possibles.append(mi) if not possibles: prints('ERROR: No results that passed all tests were found') prints('Log saved to', lf) log.close() dump_log(lf) raise SystemExit(1) good = [x for x in possibles if plugin.test_fields(x) is None] if not good: prints('Failed to find', plugin.test_fields(possibles[0])) if fail_missing_meta: raise SystemExit(1) if results[0] is not possibles[0]: prints('Most relevant result failed the tests') raise SystemExit(1) if 'cover' in plugin.capabilities: rq = Queue() mi = results[0] plugin.download_cover(log, rq, abort, title=mi.title, authors=mi.authors, identifiers=mi.identifiers) results = [] while True: try: results.append(rq.get_nowait()) except Empty: break if not results and fail_missing_meta: prints('Cover download failed') raise SystemExit(1) elif results: cdata = results[0] cover = os.path.join(tdir, plugin.name.replace(' ', '')+'-%s-cover.jpg'%sanitize_file_name(mi.title.replace(' ', '_'))) with open(cover, 'wb') as f: f.write(cdata[-1]) prints('Cover downloaded to:', cover) if len(cdata[-1]) < 10240: prints('Downloaded cover too small') raise SystemExit(1) prints('Average time per query', sum(times)/len(times)) if os.stat(lf).st_size > 10: prints('There were some errors/warnings, see log', lf)
def __init__(self, book_title, parent=None, prefs=vprefs): self.book_title = book_title self.default_file_name = sanitize_file_name(book_title[:75] + '.pdf') self.paper_size_map = {a:getattr(QPageSize, a.capitalize()) for a in PAPER_SIZES} Dialog.__init__(self, _('Print to PDF'), 'print-to-pdf', prefs=prefs, parent=parent)
def name_to_path(name): return os.path.join(config_dir, sanitize_file_name(name) + '.py')
def sanitize_icon_file_name(self, icon_path): n = lower(sanitize_file_name( os.path.splitext( os.path.basename(icon_path))[0]+'.png')) return n.replace("'", '_')
def image_filename(x): return sanitize_file_name( re.sub(r'[^0-9a-zA-Z.-]', '_', ascii_filename(x)).lstrip('_').lstrip('.'))
def ascii_filename(orig, substitute='_'): if isinstance(substitute, bytes): substitute = substitute.decode(filesystem_encoding) orig = ascii_text(orig).replace('?', '_') ans = ''.join(x if ord(x) >= 32 else substitute for x in orig) return sanitize_file_name(ans, substitute=substitute)
def library_icon_path(lib_name=''): return os.path.join( config_dir, 'library_icons', sanitize_file_name(lib_name or current_library_name()) + '.png')
def test_identify_plugin( name, tests, modify_plugin=lambda plugin: None, # {{{ fail_missing_meta=True): ''' :param name: Plugin name :param tests: List of 2-tuples. Each two tuple is of the form (args, test_funcs). args is a dict of keyword arguments to pass to the identify method. test_funcs are callables that accept a Metadata object and return True iff the object passes the test. ''' plugin = None for x in all_metadata_plugins(): if x.name == name and 'identify' in x.capabilities: plugin = x break modify_plugin(plugin) prints('Testing the identify function of', plugin.name) prints('Using extra headers:', plugin.browser.addheaders) tdir, lf, log, abort = init_test(plugin.name) prints('Log saved to', lf) times = [] for kwargs, test_funcs in tests: log('') log('#' * 80) log('### Running test with:', kwargs) log('#' * 80) prints('Running test with:', kwargs) rq = Queue() args = (log, rq, abort) start_time = time.time() plugin.running_a_test = True try: err = plugin.identify(*args, **kwargs) finally: plugin.running_a_test = False total_time = time.time() - start_time times.append(total_time) if err is not None: prints('identify returned an error for args', args) prints(err) break results = [] while True: try: results.append(rq.get_nowait()) except Empty: break prints('Found', len(results), 'matches:', end=' ') prints('Smaller relevance means better match') results.sort(key=plugin.identify_results_keygen( title=kwargs.get('title', None), authors=kwargs.get('authors', None), identifiers=kwargs.get('identifiers', {}))) for i, mi in enumerate(results): prints('*' * 30, 'Relevance:', i, '*' * 30) if mi.rating: mi.rating *= 2 prints(mi) prints('\nCached cover URL :', plugin.get_cached_cover_url(mi.identifiers)) prints('*' * 75, '\n\n') possibles = [] for mi in results: test_failed = False for tfunc in test_funcs: if not tfunc(mi): test_failed = True break if not test_failed: possibles.append(mi) if not possibles: prints('ERROR: No results that passed all tests were found') prints('Log saved to', lf) log.close() dump_log(lf) raise SystemExit(1) good = [x for x in possibles if plugin.test_fields(x) is None] if not good: prints('Failed to find', plugin.test_fields(possibles[0])) if fail_missing_meta: raise SystemExit(1) if results[0] is not possibles[0]: prints('Most relevant result failed the tests') raise SystemExit(1) if 'cover' in plugin.capabilities: rq = Queue() mi = results[0] plugin.download_cover(log, rq, abort, title=mi.title, authors=mi.authors, identifiers=mi.identifiers) results = [] while True: try: results.append(rq.get_nowait()) except Empty: break if not results and fail_missing_meta: prints('Cover download failed') raise SystemExit(1) elif results: cdata = results[0] cover = os.path.join( tdir, plugin.name.replace(' ', '') + '-%s-cover.jpg' % sanitize_file_name(mi.title.replace(' ', '_'))) with open(cover, 'wb') as f: f.write(cdata[-1]) prints('Cover downloaded to:', cover) if len(cdata[-1]) < 10240: prints('Downloaded cover too small') raise SystemExit(1) prints('Average time per query', sum(times) / len(times)) if os.stat(lf).st_size > 10: prints('There were some errors/warnings, see log', lf)
def context_menu_handler(self, action=None, category=None, key=None, index=None, search_state=None, use_vl=None): if not action: return try: if action == 'set_icon': try: path = choose_files(self, 'choose_category_icon', _('Change icon for: %s')%key, filters=[ ('Images', ['png', 'gif', 'jpg', 'jpeg'])], all_files=False, select_only_single_file=True) if path: path = path[0] p = QIcon(path).pixmap(QSize(128, 128)) d = os.path.join(config_dir, 'tb_icons') if not os.path.exists(d): os.makedirs(d) with open(os.path.join(d, 'icon_' + sanitize_file_name(key)+'.png'), 'wb') as f: f.write(pixmap_to_data(p, format='PNG')) path = os.path.basename(f.name) self._model.set_custom_category_icon(key, unicode_type(path)) self.recount() except: import traceback traceback.print_exc() return if action == 'clear_icon': self._model.set_custom_category_icon(key, None) self.recount() return if action == 'edit_item_no_vl': item = self.model().get_node(index) item.use_vl = False self.edit(index) return if action == 'edit_item_in_vl': item = self.model().get_node(index) item.use_vl = True self.edit(index) return if action == 'delete_item_in_vl': self.tag_item_delete.emit(key, index.id, index.original_name, self.model().get_book_ids_to_use()) return if action == 'delete_item_no_vl': self.tag_item_delete.emit(key, index.id, index.original_name, None) return if action == 'open_editor': self.tags_list_edit.emit(category, key) return if action == 'manage_categories': self.edit_user_category.emit(category) return if action == 'search': self._toggle(index, set_to=search_state) return if action == "raw_search": from calibre.gui2.ui import get_gui get_gui().get_saved_search_text(search_name='search:' + key) return if action == 'add_to_category': tag = index.tag if len(index.children) > 0: for c in index.all_children(): self.add_item_to_user_cat.emit(category, c.tag.original_name, c.tag.category) self.add_item_to_user_cat.emit(category, tag.original_name, tag.category) return if action == 'add_subcategory': self.add_subcategory.emit(key) return if action == 'search_category': self._toggle(index, set_to=search_state) return if action == 'delete_user_category': self.delete_user_category.emit(key) return if action == 'delete_search': self.model().db.saved_search_delete(key) self.rebuild_saved_searches.emit() return if action == 'delete_item_from_user_category': tag = index.tag if len(index.children) > 0: for c in index.children: self.del_item_from_user_cat.emit(key, c.tag.original_name, c.tag.category) self.del_item_from_user_cat.emit(key, tag.original_name, tag.category) return if action == 'manage_searches': self.saved_search_edit.emit(category) return if action == 'edit_author_sort': self.author_sort_edit.emit(self, index, True, False) return if action == 'edit_author_link': self.author_sort_edit.emit(self, index, False, True) return reset_filter_categories = True if action == 'hide': self.hidden_categories.add(category) elif action == 'show': self.hidden_categories.discard(category) elif action == 'categorization': changed = self.collapse_model != category self._model.collapse_model = category if changed: reset_filter_categories = False gprefs['tags_browser_partition_method'] = category elif action == 'defaults': self.hidden_categories.clear() self.db.new_api.set_pref('tag_browser_hidden_categories', list(self.hidden_categories)) if reset_filter_categories: self._model.set_categories_filter(None) self._model.rebuild_node_tree() except: return
def catalog_generated(self, job): if job.result: # Problems during catalog generation # jobs.results is a list - the first entry is the intended title for the dialog # Subsequent strings are error messages dialog_title = job.result.pop(0) if re.search('warning', job.result[0].lower()): msg = _("Catalog generation complete, with warnings.") warning_dialog(self.gui, dialog_title, msg, det_msg='\n'.join(job.result), show=True) else: job.result.append("Catalog generation terminated.") error_dialog(self.gui, dialog_title, '\n'.join(job.result), show=True) return if job.failed: return self.gui.job_exception(job) if dynamic.get('catalog_add_to_library', True): id = self.gui.library_view.model().add_catalog( job.catalog_file_path, job.catalog_title) self.gui.library_view.model().beginResetModel( ), self.gui.library_view.model().endResetModel() if job.catalog_sync: sync = dynamic.get('catalogs_to_be_synced', set()) sync.add(id) dynamic.set('catalogs_to_be_synced', sync) self.gui.status_bar.show_message(_('Catalog generated.'), 3000) self.gui.sync_catalogs() if not dynamic.get('catalog_add_to_library', True) or job.fmt not in { 'EPUB', 'MOBI', 'AZW3' }: export_dir = choose_dir( self.gui, _('Export catalog folder'), _('Select destination for %(title)s.%(fmt)s') % dict(title=job.catalog_title, fmt=job.fmt.lower())) if export_dir: destination = os.path.join( export_dir, '{}.{}'.format(sanitize_file_name(job.catalog_title), job.fmt.lower())) try: shutil.copyfile(job.catalog_file_path, destination) except OSError as err: if getattr(err, 'errno', None) == errno.EACCES: # Permission denied import traceback error_dialog( self.gui, _('Permission denied'), _('Could not open %s. Is it being used by another' ' program?') % destination, det_msg=traceback.format_exc(), show=True) return raise