diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 6016936cfc..877c89cb96 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -7,26 +7,27 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os -from functools import partial from collections import defaultdict - -from PyQt5.Qt import QPixmap, QTimer, QApplication, QDialog +from functools import partial +from PyQt5.Qt import QApplication, QDialog, QPixmap, QTimer from calibre import as_unicode, guess_type -from calibre.gui2 import (error_dialog, choose_files, choose_dir, - warning_dialog, info_dialog, gprefs) +from calibre.constants import iswindows +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.ebooks.metadata import MetaInformation +from calibre.gui2 import ( + choose_dir, choose_files, error_dialog, gprefs, info_dialog, question_dialog, + warning_dialog +) +from calibre.gui2.actions import InterfaceAction from calibre.gui2.dialogs.add_empty_book import AddEmptyBookDialog from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.progress import ProgressDialog -from calibre.ebooks import BOOK_EXTENSIONS +from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config_base import tweaks from calibre.utils.filenames import ascii_filename, make_long_path_useable from calibre.utils.icu import sort_key -from calibre.gui2.actions import InterfaceAction -from calibre.gui2 import question_dialog -from calibre.ebooks.metadata import MetaInformation -from calibre.ptempfile import PersistentTemporaryFile -from polyglot.builtins import iteritems, string_or_bytes, range +from polyglot.builtins import iteritems, range, string_or_bytes def get_filters(): @@ -430,6 +431,9 @@ class AddAction(InterfaceAction): formats = [] from calibre.gui2.dnd import image_extensions image_exts = set(image_extensions()) - set(tweaks['cover_drop_exclude']) + if iswindows: + from calibre.gui2.add import resolve_windows_links + paths = list(resolve_windows_links(paths, hwnd=int(self.gui.effectiveWinId()))) for path in paths: ext = os.path.splitext(path)[1].lower() if ext: diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py index 3215e0e8f7..30422e0d10 100644 --- a/src/calibre/gui2/add.py +++ b/src/calibre/gui2/add.py @@ -7,6 +7,7 @@ __copyright__ = '2014, Kovid Goyal ' import os import shutil +import sys import tempfile import time import traceback @@ -61,6 +62,24 @@ def validate_source(source, parent=None): # {{{ # }}} +def resolve_windows_links(paths, hwnd=None): + try: + from calibre_extensions.winutil import resolve_lnk + except ImportError: + return paths + for x in paths: + if x.lower().endswith('.lnk'): + try: + if hwnd is None: + x = resolve_lnk(x) + else: + x = resolve_lnk(x, 0, hwnd) + except Exception as e: + print('Failed to resolve link', x, 'with error:', e, file=sys.stderr) + continue + yield x + + class Adder(QObject): do_one_signal = pyqtSignal() @@ -85,6 +104,11 @@ class Adder(QObject): self.do_one_signal.connect(self.tick, type=Qt.ConnectionType.QueuedConnection) self.pool = pool self.pd = ProgressDialog(_('Adding books...'), _('Scanning for files...'), min=0, max=0, parent=parent, icon='add_book.png') + self.win_id = None + if parent is not None and hasattr(parent, 'effectiveWinId'): + self.win_id = parent.effectiveWinId() + if self.win_id is not None: + self.win_id = int(self.win_id) self.db = getattr(db, 'new_api', None) if self.db is not None: self.dbref = weakref.ref(db) @@ -149,7 +173,10 @@ class Adder(QObject): for files in find_books_in_directory(dirpath, self.single_book_per_directory, compiled_rules=compiled_rules): if self.abort_scan: return - self.file_groups[len(self.file_groups)] = files + if iswindows: + files = list(resolve_windows_links(files, hwnd=self.win_id)) + if files: + self.file_groups[len(self.file_groups)] = files else: def find_files(root): if isinstance(root, unicode_type): @@ -195,7 +222,13 @@ class Adder(QObject): find_files(extract(path)) self.ignore_opf = True else: - self.file_groups[len(self.file_groups)] = [path] + x = [path] + if iswindows: + x = list(resolve_windows_links(x, hwnd=self.win_id)) + if x: + self.file_groups[len(self.file_groups)] = x + else: + unreadable_files.append(path) else: unreadable_files.append(path) if unreadable_files: diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 03dae13d9a..5e3dd76408 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -20,6 +20,7 @@ from PyQt5.Qt import ( ) from calibre import strftime +from calibre.constants import iswindows from calibre.customize.ui import run_plugins_on_import from calibre.db import SPOOL_SIZE from calibre.ebooks import BOOK_EXTENSIONS @@ -973,6 +974,11 @@ class FormatsManager(QWidget): bad_perms = [] for _file in paths: _file = make_long_path_useable(os.path.abspath(_file)) + if iswindows: + from calibre.gui2.add import resolve_windows_links + x = list(resolve_windows_links([_file], hwnd=int(self.effectiveWinId()))) + if x: + _file = x[0] if not os.access(_file, os.R_OK): bad_perms.append(_file) continue diff --git a/src/calibre/utils/windows/common.h b/src/calibre/utils/windows/common.h index 40f36f40a6..dd45f1a797 100644 --- a/src/calibre/utils/windows/common.h +++ b/src/calibre/utils/windows/common.h @@ -11,6 +11,7 @@ #include #include #include +#define arraysz(x) (sizeof(x)/sizeof(x[0])) static inline PyObject* set_error_from_hresult(const char *file, const int line, const HRESULT hr, const char *prefix="", PyObject *name=NULL) { diff --git a/src/calibre/utils/windows/winutil.cpp b/src/calibre/utils/windows/winutil.cpp index 465897dc1c..0c71f5616f 100644 --- a/src/calibre/utils/windows/winutil.cpp +++ b/src/calibre/utils/windows/winutil.cpp @@ -675,6 +675,44 @@ winutil_move_to_trash(PyObject *self, PyObject *args) { Py_RETURN_NONE; } +static PyObject* +resolve_lnk(PyObject *self, PyObject *args) { + wchar_raii path; + HRESULT hr; + PyObject *win_id = NULL; + unsigned short timeout = 0; + if (!PyArg_ParseTuple(args, "O&|HO!", py_to_wchar, &path, &timeout, &PyLong_Type, &win_id)) return NULL; + if (!path.ptr()) { + PyErr_SetString(PyExc_TypeError, "Path must not be None"); + return NULL; + } + scoped_com_initializer com; + if (!com.succeded()) { PyErr_SetString(PyExc_OSError, "Failed to initialize COM"); return NULL; } + CComPtr shell_link; + if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shell_link)))) { + PyErr_SetString(PyExc_OSError, "Failed to create IShellLink instance"); + return NULL; + } + CComPtr persist_file; + if (FAILED(shell_link->QueryInterface(IID_PPV_ARGS(&persist_file)))) { + PyErr_SetString(PyExc_OSError, "Failed to create IPersistFile instance"); + return NULL; + } + hr = persist_file->Load(path.ptr(), 0); + if (FAILED(hr)) return error_from_hresult(hr, "Failed to load link"); + DWORD flags = SLR_UPDATE | ( (timeout & 0xffff) << 16 ); + if (win_id) { + hr = shell_link->Resolve(static_cast(PyLong_AsVoidPtr(win_id)), flags); + } else { + hr = shell_link->Resolve(NULL, flags | SLR_NO_UI | SLR_NOTRACK | SLR_NOLINKINFO); + } + if (FAILED(hr)) return error_from_hresult(hr, "Failed to resolve link"); + wchar_t buf[2048]; + hr = shell_link->GetPath(buf, arraysz(buf), NULL, 0); + if (FAILED(hr)) return error_from_hresult(hr, "Failed to get path from link"); + return PyUnicode_FromWideChar(buf, -1); +} + static PyObject * winutil_manage_shortcut(PyObject *self, PyObject *args) { wchar_raii path, target, description, quoted_args; @@ -701,7 +739,7 @@ winutil_manage_shortcut(PyObject *self, PyObject *args) { if (!target.ptr()) { wchar_t buf[2048]; if (FAILED(persist_file->Load(path.ptr(), 0))) Py_RETURN_NONE; - if (FAILED(shell_link->GetPath(buf, sizeof(buf), NULL, 0))) Py_RETURN_NONE; + if (FAILED(shell_link->GetPath(buf, arraysz(buf), NULL, 0))) Py_RETURN_NONE; return Py_BuildValue("u", buf); } @@ -1172,6 +1210,10 @@ static PyMethodDef winutil_methods[] = { "manage_shortcut()\n\nManage a shortcut" }, + {"resolve_lnk", (PyCFunction)resolve_lnk, METH_VARARGS, + "resolve_lnk()\n\nGet the target of a lnk file." + }, + {"get_file_id", (PyCFunction)winutil_get_file_id, METH_VARARGS, "get_file_id(path)\n\nGet the windows file id (volume_num, file_index_high, file_index_low)" },