From f19ca06b86e0ac91d53673da39814501a1a727ce Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 20 Jan 2025 14:16:20 +0530 Subject: [PATCH] Start work on names based interface for windows mtp driver --- .../mtp/windows/content_enumeration.cpp | 114 ++++++++++++++++-- src/calibre/devices/mtp/windows/device.cpp | 35 ++++++ src/calibre/devices/mtp/windows/driver.py | 9 ++ src/calibre/devices/mtp/windows/global.h | 5 +- 4 files changed, 151 insertions(+), 12 deletions(-) diff --git a/src/calibre/devices/mtp/windows/content_enumeration.cpp b/src/calibre/devices/mtp/windows/content_enumeration.cpp index 7c2e926585..b7ea6eaddf 100644 --- a/src/calibre/devices/mtp/windows/content_enumeration.cpp +++ b/src/calibre/devices/mtp/windows/content_enumeration.cpp @@ -32,7 +32,7 @@ pump_waiting_messages() { } static IPortableDeviceKeyCollection* -create_filesystem_properties_collection() { // {{{ +create_filesystem_properties_collection(bool for_name_query = false) { // {{{ CComPtr properties; HRESULT hr; @@ -45,17 +45,19 @@ create_filesystem_properties_collection() { // {{{ #define ADDPROP(x) hr = properties->Add(x); if (FAILED(hr)) { hresult_set_exc("Failed to add property " #x " to filesystem properties collection", hr); return NULL; } ADDPROP(WPD_OBJECT_CONTENT_TYPE); - ADDPROP(WPD_OBJECT_PARENT_ID); - ADDPROP(WPD_OBJECT_PERSISTENT_UNIQUE_ID); - ADDPROP(WPD_OBJECT_NAME); ADDPROP(WPD_OBJECT_ORIGINAL_FILE_NAME); - // ADDPROP(WPD_OBJECT_SYNC_ID); - ADDPROP(WPD_OBJECT_ISSYSTEM); - ADDPROP(WPD_OBJECT_ISHIDDEN); - ADDPROP(WPD_OBJECT_CAN_DELETE); - ADDPROP(WPD_OBJECT_SIZE); - ADDPROP(WPD_OBJECT_DATE_CREATED); - ADDPROP(WPD_OBJECT_DATE_MODIFIED); + if (!for_name_query) { + ADDPROP(WPD_OBJECT_PARENT_ID); + ADDPROP(WPD_OBJECT_PERSISTENT_UNIQUE_ID); + ADDPROP(WPD_OBJECT_NAME); + // ADDPROP(WPD_OBJECT_SYNC_ID); + ADDPROP(WPD_OBJECT_ISSYSTEM); + ADDPROP(WPD_OBJECT_ISHIDDEN); + ADDPROP(WPD_OBJECT_CAN_DELETE); + ADDPROP(WPD_OBJECT_SIZE); + ADDPROP(WPD_OBJECT_DATE_CREATED); + ADDPROP(WPD_OBJECT_DATE_MODIFIED); + } #undef ADDPROP return properties.Detach(); @@ -363,6 +365,23 @@ find_objects_in(CComPtr &content, CComPtr values; + HRESULT hr; + + Py_BEGIN_ALLOW_THREADS; + hr = devprops->GetValues(object_id, properties, &values); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) { hresult_set_exc("Failed to get properties for object", hr); return NULL; } + com_wchar_raii property; + hr = values->GetStringValue(WPD_OBJECT_ORIGINAL_FILE_NAME, property.unsafe_address()); + if (SUCCEEDED(hr)) return PyUnicode_FromWideChar(property.ptr(), -1); + hresult_set_exc("Failed to get original file name for object", hr); + return NULL; +} + + static PyObject* get_object_properties(IPortableDeviceProperties *devprops, IPortableDeviceKeyCollection *properties, const wchar_t *object_id) { CComPtr values; @@ -448,6 +467,79 @@ create_object_properties(const wchar_t *parent_id, const wchar_t *name, const GU return values.Detach(); } // }}} +PyObject* +list_folder(CComPtr &content, IPortableDevicePropertiesBulk *bulk_properties, const wchar_t *folder_id) { + HRESULT hr; + CComPtr object_ids; + Py_BEGIN_ALLOW_THREADS; + hr = object_ids.CoCreateInstance(CLSID_PortableDevicePropVariantCollection, NULL, CLSCTX_INPROC_SERVER); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) { hresult_set_exc("Failed to create propvariantcollection", hr); return NULL; } + + bool enum_failed = false; + if (!find_objects_in(content, object_ids, folder_id, &enum_failed)) return NULL; + DWORD num; + CComPtr devprops; + + hr = content->Properties(&devprops); + if (FAILED(hr)) { hresult_set_exc("Failed to get IPortableDeviceProperties interface", hr); return NULL; } + + CComPtr properties(create_filesystem_properties_collection()); + if (!properties) return NULL; + hr = object_ids->GetCount(&num); + if (FAILED(hr)) { hresult_set_exc("Failed to get object id count", hr); return NULL; } + pyobject_raii ans(PyList_New(0)); if (!ans) return NULL; + + for (DWORD i = 0; i < num; i++) { + prop_variant pv; + hr = object_ids->GetAt(i, &pv); + if (SUCCEEDED(hr) && pv.pwszVal != NULL) { + pyobject_raii item(get_object_properties(devprops, properties, pv.pwszVal)); + if (!item) return NULL; + if (PyList_Append(ans.ptr(), item.ptr()) != 0) return NULL; + } + } + return ans.detach(); +} + +PyObject* +find_in_parent(CComPtr &content, IPortableDevicePropertiesBulk *bulk_properties, const wchar_t *parent_id, PyObject *name) { + HRESULT hr; + CComPtr object_ids; + Py_BEGIN_ALLOW_THREADS; + hr = object_ids.CoCreateInstance(CLSID_PortableDevicePropVariantCollection, NULL, CLSCTX_INPROC_SERVER); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) { hresult_set_exc("Failed to create propvariantcollection", hr); return NULL; } + + bool enum_failed = false; + if (!find_objects_in(content, object_ids, parent_id, &enum_failed)) return NULL; + DWORD num; + CComPtr devprops; + + hr = content->Properties(&devprops); + if (FAILED(hr)) { hresult_set_exc("Failed to get IPortableDeviceProperties interface", hr); return NULL; } + + CComPtr properties(create_filesystem_properties_collection(true)); + if (!properties) return NULL; + + hr = object_ids->GetCount(&num); + if (FAILED(hr)) { hresult_set_exc("Failed to get object id count", hr); return NULL; } + + for (DWORD i = 0; i < num; i++) { + prop_variant pv; + hr = object_ids->GetAt(i, &pv); + if (SUCCEEDED(hr) && pv.pwszVal != NULL) { + pyobject_raii item(get_object_filename(devprops, properties, pv.pwszVal)); + if (!item) { if (PyErr_Occurred()) { PyErr_Clear(); }; continue; } + pyobject_raii q(PyObject_CallMethod(item.ptr(), "lower", NULL)); + if (!q) return NULL; + if (PyUnicode_Compare(q.ptr(), name) == 0) return PyUnicode_FromWideChar(pv.pwszVal, -1); + } + } + return NULL; +} + + static bool get_files_and_folders(unsigned int level, IPortableDevice *device, CComPtr &content, IPortableDevicePropertiesBulk *bulk_properties, const wchar_t *parent_id, PyObject *callback, PyObject *ans) { // {{{ CComPtr object_ids; diff --git a/src/calibre/devices/mtp/windows/device.cpp b/src/calibre/devices/mtp/windows/device.cpp index 41f9b464c9..dd2809b8b3 100644 --- a/src/calibre/devices/mtp/windows/device.cpp +++ b/src/calibre/devices/mtp/windows/device.cpp @@ -78,6 +78,37 @@ py_get_file(Device *self, PyObject *args) { return wpd::get_file(self->device, object.ptr(), stream, callback); } // }}} +// list_folder_by_name() {{{ + +static PyObject* +list_folder_by_name(Device *self, PyObject *args) { + wchar_raii parent_id; PyObject *names; + CComPtr content; + HRESULT hr; bool found = false; + + Py_BEGIN_ALLOW_THREADS; + hr = self->device->Content(&content); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) { hresult_set_exc("Failed to create content interface", hr); return NULL; } + + + if (!PyArg_ParseTuple(args, "O&O!", py_to_wchar, &parent_id, &PyTuple_Type, &names)) return NULL; + for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(names); i++) { + PyObject *k = PyTuple_GET_ITEM(names, i); + if (!PyUnicode_Check(k)) { PyErr_SetString(PyExc_TypeError, "names must contain only unicode strings"); return NULL; } + pyobject_raii l(PyObject_CallMethod(k, "lower", NULL)); if (!l) return NULL; + pyobject_raii object_id(wpd::find_in_parent(content, self->bulk_properties, parent_id.ptr(), l.ptr())); + if (!object_id) { + if (PyErr_Occurred()) return NULL; + Py_RETURN_NONE; + } + if (!py_to_wchar_(object_id.ptr(), &parent_id)) return NULL; + found = true; + } + if (!found) Py_RETURN_NONE; + return wpd::list_folder(content, self->bulk_properties, parent_id.ptr()); +} // }}} + // create_folder() {{{ static PyObject* py_create_folder(Device *self, PyObject *args) { @@ -116,6 +147,10 @@ static PyMethodDef Device_methods[] = { "get_filesystem(storage_id, callback) -> Get all files/folders on the storage identified by storage_id. Tries to use bulk operations when possible. callback must be a callable that is called as (object, level). It is called with every found object. If the callback returns False and the object is a folder, it is not recursed into." }, + {"list_folder_by_name", (PyCFunction)list_folder_by_name, METH_VARARGS, + "list_folder_by_name(parent_id, names) -> List the folder specified by names (a tuple of name components) relative to parent_id from the device. Return None or a list of entries." + }, + {"get_file", (PyCFunction)py_get_file, METH_VARARGS, "get_file(object_id, stream, callback=None) -> Get the file identified by object_id from the device. The file is written to the stream object, which must be a file like object. If callback is not None, it must be a callable that accepts two arguments: (bytes_read, total_size). It will be called after each chunk is read from the device. Note that it can be called multiple times with the same values." }, diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index fdf615956a..e8af41de37 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -391,6 +391,15 @@ class MTP_DEVICE(MTPDeviceBase): ans[i] = s['free_space'] return tuple(ans) + @same_thread + def list_mtp_folder_by_name(self, parent, *names: str): + if not parent.is_folder: + raise ValueError(f'{parent.full_path} is not a folder') + x = self.dev.list_folder_by_name(parent.object_id, names) + if x is None: + raise DeviceError(f'Could not find folder named: {"/".join(names)} in {parent.full_path}') + return x + @same_thread def get_mtp_file(self, f, stream=None, callback=None): if f.is_folder: diff --git a/src/calibre/devices/mtp/windows/global.h b/src/calibre/devices/mtp/windows/global.h index 1c2fb779b6..d85aa91d26 100644 --- a/src/calibre/devices/mtp/windows/global.h +++ b/src/calibre/devices/mtp/windows/global.h @@ -49,5 +49,8 @@ extern PyObject* get_file(IPortableDevice *device, const wchar_t *object_id, PyO extern PyObject* create_folder(IPortableDevice *device, const wchar_t *parent_id, const wchar_t *name); extern PyObject* delete_object(IPortableDevice *device, const wchar_t *object_id); extern PyObject* put_file(IPortableDevice *device, const wchar_t *parent_id, const wchar_t *name, PyObject *src, unsigned PY_LONG_LONG size, PyObject *callback); - +extern PyObject* find_in_parent( + CComPtr &content, IPortableDevicePropertiesBulk *bulk_properties, const wchar_t *parent_id, PyObject *name); +extern PyObject* list_folder( + CComPtr &content, IPortableDevicePropertiesBulk *bulk_properties, const wchar_t *folder_id); }