Windows: Automatically resolve shortcuts (.lnk files) when adding books to calibre. Fixes #1907410 [Enhancement request: Dragging a shortcut file into Calibre should add target file](https://bugs.launchpad.net/calibre/+bug/1907410)

This commit is contained in:
Kovid Goyal 2020-12-14 16:39:10 +05:30
parent 8b4dae7c9e
commit 74f7adb510
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 100 additions and 14 deletions

View File

@ -7,26 +7,27 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__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:

View File

@ -7,6 +7,7 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
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,6 +173,9 @@ 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
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):
@ -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:

View File

@ -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

View File

@ -11,6 +11,7 @@
#include <Windows.h>
#include <Python.h>
#include <comdef.h>
#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) {

View File

@ -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<IShellLink> 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<IPersistFile> 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<HWND>(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)"
},