1 /* This file is part of Clementine.
2 Copyright 2010, David Sansome <me@davidsansome.com>
3
4 Clementine is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 Clementine is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with Clementine. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "config.h"
19
20 #include <functional>
21 #include <memory>
22 #include <functional> // for std::placeholders
23
24 #include <QFile>
25 #include <QStringList>
26 #include <QUrlQuery>
27 #include <QtDebug>
28
29 #include "giolister.h"
30 #include "core/logging.h"
31 #include "core/signalchecker.h"
32
33 using std::placeholders::_1;
34 using std::placeholders::_2;
35 using std::placeholders::_3;
36
unique_id() const37 QString GioLister::DeviceInfo::unique_id() const {
38 if (mount)
39 return QString("Gio/%1/%2/%3").arg(mount_uuid, filesystem_type).arg(
40 filesystem_size);
41
42 return QString("Gio/unmounted/%1").arg((qulonglong)volume.get());
43 }
44
is_suitable() const45 bool GioLister::DeviceInfo::is_suitable() const {
46 if (!volume) return false; // This excludes smb or ssh mounts
47
48 if (drive && !drive_removable) return false; // This excludes internal drives
49
50 if (filesystem_type.isEmpty()) return true;
51
52 return filesystem_type != "udf" && filesystem_type != "smb" &&
53 filesystem_type != "cifs" && filesystem_type != "ssh" &&
54 filesystem_type != "isofs";
55 }
56
57 template <typename T, typename F>
OperationFinished(F f,GObject * object,GAsyncResult * result)58 void OperationFinished(F f, GObject* object, GAsyncResult* result) {
59 T* obj = reinterpret_cast<T*>(object);
60 GError* error = nullptr;
61
62 f(obj, result, &error);
63
64 if (error) {
65 qLog(Error) << "Mount/unmount error:"
66 << QString::fromLocal8Bit(error->message);
67 g_error_free(error);
68 }
69 }
70
VolumeMountFinished(GObject * object,GAsyncResult * result,gpointer)71 void GioLister::VolumeMountFinished(GObject* object, GAsyncResult* result,
72 gpointer) {
73 OperationFinished<GVolume>(std::bind(g_volume_mount_finish, _1, _2, _3),
74 object, result);
75 }
76
Init()77 void GioLister::Init() {
78 monitor_.reset_without_add(g_volume_monitor_get());
79
80 // Get existing volumes
81 GList* const volumes = g_volume_monitor_get_volumes(monitor_);
82 for (GList* p = volumes; p; p = p->next) {
83 GVolume* volume = static_cast<GVolume*>(p->data);
84
85 VolumeAdded(volume);
86 g_object_unref(volume);
87 }
88 g_list_free(volumes);
89
90 // Get existing mounts
91 GList* const mounts = g_volume_monitor_get_mounts(monitor_);
92 for (GList* p = mounts; p; p = p->next) {
93 GMount* mount = static_cast<GMount*>(p->data);
94
95 MountAdded(mount);
96 g_object_unref(mount);
97 }
98 g_list_free(mounts);
99
100 // Connect signals from the monitor
101 signals_.append(CHECKED_GCONNECT(monitor_, "volume-added", &VolumeAddedCallback, this));
102 signals_.append(CHECKED_GCONNECT(monitor_, "volume-removed", &VolumeRemovedCallback, this));
103 signals_.append(CHECKED_GCONNECT(monitor_, "mount-added", &MountAddedCallback, this));
104 signals_.append(CHECKED_GCONNECT(monitor_, "mount-changed", &MountChangedCallback, this));
105 signals_.append(CHECKED_GCONNECT(monitor_, "mount-removed", &MountRemovedCallback, this));
106 }
107
~GioLister()108 GioLister::~GioLister() {
109 for (gulong signal : signals_) {
110 g_signal_handler_disconnect(monitor_, signal);
111 }
112 }
113
DeviceUniqueIDs()114 QStringList GioLister::DeviceUniqueIDs() {
115 QMutexLocker l(&mutex_);
116 return devices_.keys();
117 }
118
DeviceIcons(const QString & id)119 QVariantList GioLister::DeviceIcons(const QString& id) {
120 QVariantList ret;
121 QMutexLocker l(&mutex_);
122 if (!devices_.contains(id)) return ret;
123
124 const DeviceInfo& info = devices_[id];
125
126 if (info.mount) {
127 ret << DeviceLister::GuessIconForPath(info.mount_path);
128 ret << info.mount_icon_names;
129 }
130
131 ret << DeviceLister::GuessIconForModel(QString(), info.mount_name);
132
133 return ret;
134 }
135
DeviceManufacturer(const QString & id)136 QString GioLister::DeviceManufacturer(const QString& id) { return QString(); }
137
DeviceModel(const QString & id)138 QString GioLister::DeviceModel(const QString& id) {
139 QMutexLocker l(&mutex_);
140 if (!devices_.contains(id)) return QString();
141 const DeviceInfo& info = devices_[id];
142
143 return info.drive_name.isEmpty() ? info.volume_name : info.drive_name;
144 }
145
DeviceCapacity(const QString & id)146 quint64 GioLister::DeviceCapacity(const QString& id) {
147 return LockAndGetDeviceInfo(id, &DeviceInfo::filesystem_size);
148 }
149
DeviceFreeSpace(const QString & id)150 quint64 GioLister::DeviceFreeSpace(const QString& id) {
151 return LockAndGetDeviceInfo(id, &DeviceInfo::filesystem_free);
152 }
153
MakeFriendlyName(const QString & id)154 QString GioLister::MakeFriendlyName(const QString& id) {
155 return DeviceModel(id);
156 }
157
DeviceHardwareInfo(const QString & id)158 QVariantMap GioLister::DeviceHardwareInfo(const QString& id) {
159 QVariantMap ret;
160
161 QMutexLocker l(&mutex_);
162 if (!devices_.contains(id)) return ret;
163 const DeviceInfo& info = devices_[id];
164
165 ret[QT_TR_NOOP("Mount point")] = info.mount_path;
166 ret[QT_TR_NOOP("Device")] = info.volume_unix_device;
167 ret[QT_TR_NOOP("URI")] = info.mount_uri;
168 return ret;
169 }
170
MakeDeviceUrls(const QString & id)171 QList<QUrl> GioLister::MakeDeviceUrls(const QString& id) {
172 QString mount_point;
173 QString uri;
174 QString unix_device;
175 {
176 QMutexLocker l(&mutex_);
177 mount_point = devices_[id].mount_path;
178 uri = devices_[id].mount_uri;
179 unix_device = devices_[id].volume_unix_device;
180 }
181
182 // gphoto2 gives invalid hostnames with []:, characters in
183 uri.replace(QRegExp("//\\[usb:(\\d+),(\\d+)\\]"), "//usb-\\1-\\2");
184
185 QUrl url(uri);
186
187 QList<QUrl> ret;
188
189 if (url.isValid()) {
190 QRegExp device_re("usb/(\\d+)/(\\d+)");
191 if (device_re.indexIn(unix_device) >= 0) {
192 QUrlQuery url_query(url);
193 url_query.addQueryItem("busnum", device_re.cap(1));
194 url_query.addQueryItem("devnum", device_re.cap(2));
195 url.setQuery(url_query);
196 }
197
198 // Special case for file:// GIO URIs - we have to check whether they point
199 // to an ipod.
200 if (url.scheme() == "file") {
201 ret << MakeUrlFromLocalPath(url.path());
202 } else {
203 ret << url;
204 }
205 }
206
207 ret << MakeUrlFromLocalPath(mount_point);
208 return ret;
209 }
210
VolumeAddedCallback(GVolumeMonitor *,GVolume * v,gpointer d)211 void GioLister::VolumeAddedCallback(GVolumeMonitor*, GVolume* v, gpointer d) {
212 static_cast<GioLister*>(d)->VolumeAdded(v);
213 }
214
VolumeRemovedCallback(GVolumeMonitor *,GVolume * v,gpointer d)215 void GioLister::VolumeRemovedCallback(GVolumeMonitor*, GVolume* v, gpointer d) {
216 static_cast<GioLister*>(d)->VolumeRemoved(v);
217 }
218
MountAddedCallback(GVolumeMonitor *,GMount * m,gpointer d)219 void GioLister::MountAddedCallback(GVolumeMonitor*, GMount* m, gpointer d) {
220 static_cast<GioLister*>(d)->MountAdded(m);
221 }
222
MountChangedCallback(GVolumeMonitor *,GMount * m,gpointer d)223 void GioLister::MountChangedCallback(GVolumeMonitor*, GMount* m, gpointer d) {
224 static_cast<GioLister*>(d)->MountChanged(m);
225 }
226
MountRemovedCallback(GVolumeMonitor *,GMount * m,gpointer d)227 void GioLister::MountRemovedCallback(GVolumeMonitor*, GMount* m, gpointer d) {
228 static_cast<GioLister*>(d)->MountRemoved(m);
229 }
230
VolumeAdded(GVolume * volume)231 void GioLister::VolumeAdded(GVolume* volume) {
232 g_object_ref(volume);
233
234 DeviceInfo info;
235 info.ReadVolumeInfo(volume);
236 #ifdef HAVE_AUDIOCD
237 if (info.volume_root_uri.startsWith("cdda"))
238 // Audio CD devices are already handled by CDDA lister
239 return;
240 #endif
241 info.ReadDriveInfo(g_volume_get_drive(volume));
242 info.ReadMountInfo(g_volume_get_mount(volume));
243 if (!info.is_suitable()) return;
244
245 {
246 QMutexLocker l(&mutex_);
247 devices_[info.unique_id()] = info;
248 }
249
250 emit DeviceAdded(info.unique_id());
251 }
252
VolumeRemoved(GVolume * volume)253 void GioLister::VolumeRemoved(GVolume* volume) {
254 QString id;
255 {
256 QMutexLocker l(&mutex_);
257 id = FindUniqueIdByVolume(volume);
258 if (id.isNull()) return;
259
260 devices_.remove(id);
261 }
262
263 emit DeviceRemoved(id);
264 }
265
MountAdded(GMount * mount)266 void GioLister::MountAdded(GMount* mount) {
267 g_object_ref(mount);
268
269 DeviceInfo info;
270 info.ReadVolumeInfo(g_mount_get_volume(mount));
271 #ifdef HAVE_AUDIOCD
272 if (info.volume_root_uri.startsWith("cdda"))
273 // Audio CD devices are already handled by CDDA lister
274 return;
275 #endif
276 info.ReadMountInfo(mount);
277 info.ReadDriveInfo(g_mount_get_drive(mount));
278 if (!info.is_suitable()) return;
279
280 QString old_id;
281 {
282 QMutexLocker l(&mutex_);
283
284 // The volume might already exist - either mounted or unmounted.
285 for (const QString& id : devices_.keys()) {
286 if (devices_[id].volume == info.volume) {
287 old_id = id;
288 break;
289 }
290 }
291
292 if (!old_id.isEmpty() && old_id != info.unique_id()) {
293 // If the ID has changed (for example, after it's been mounted), we need
294 // to remove the old device.
295 devices_.remove(old_id);
296 emit DeviceRemoved(old_id);
297
298 old_id = QString();
299 }
300 devices_[info.unique_id()] = info;
301 }
302
303 if (!old_id.isEmpty())
304 emit DeviceChanged(old_id);
305 else {
306 emit DeviceAdded(info.unique_id());
307 }
308 }
309
MountChanged(GMount * mount)310 void GioLister::MountChanged(GMount* mount) {
311 QString id;
312 {
313 QMutexLocker l(&mutex_);
314 id = FindUniqueIdByMount(mount);
315 if (id.isNull()) return;
316
317 g_object_ref(mount);
318
319 DeviceInfo new_info;
320 new_info.ReadMountInfo(mount);
321 new_info.ReadVolumeInfo(g_mount_get_volume(mount));
322 new_info.ReadDriveInfo(g_mount_get_drive(mount));
323
324 // Ignore the change if the new info is useless
325 if (new_info.invalid_enclosing_mount ||
326 (devices_[id].filesystem_size != 0 && new_info.filesystem_size == 0) ||
327 (!devices_[id].filesystem_type.isEmpty() &&
328 new_info.filesystem_type.isEmpty()))
329 return;
330
331 devices_[id] = new_info;
332 }
333
334 emit DeviceChanged(id);
335 }
336
MountRemoved(GMount * mount)337 void GioLister::MountRemoved(GMount* mount) {
338 QString id;
339 {
340 QMutexLocker l(&mutex_);
341 id = FindUniqueIdByMount(mount);
342 if (id.isNull()) return;
343
344 devices_.remove(id);
345 }
346
347 emit DeviceRemoved(id);
348 }
349
ConvertAndFree(char * str)350 QString GioLister::DeviceInfo::ConvertAndFree(char* str) {
351 QString ret = QString::fromUtf8(str);
352 g_free(str);
353 return ret;
354 }
355
ReadMountInfo(GMount * mount)356 void GioLister::DeviceInfo::ReadMountInfo(GMount* mount) {
357 // Get basic information
358 this->mount.reset_without_add(mount);
359 if (!mount) return;
360
361 mount_name = ConvertAndFree(g_mount_get_name(mount));
362
363 // Get the icon name(s)
364 mount_icon_names.clear();
365 GIcon* icon = g_mount_get_icon(mount);
366 if (G_IS_THEMED_ICON(icon)) {
367 const char* const* icons = g_themed_icon_get_names(G_THEMED_ICON(icon));
368 for (const char* const* p = icons; *p; ++p) {
369 mount_icon_names << QString::fromUtf8(*p);
370 }
371 }
372 g_object_unref(icon);
373
374 GFile* root = g_mount_get_root(mount);
375
376 // Get the mount path
377 mount_path = ConvertAndFree(g_file_get_path(root));
378 mount_uri = ConvertAndFree(g_file_get_uri(root));
379
380 // Do a sanity check to make sure the root is actually this mount - when a
381 // device is unmounted GIO sends a changed signal before the removed signal,
382 // and we end up reading information about the / filesystem by mistake.
383 GError* error = nullptr;
384 GMount* actual_mount = g_file_find_enclosing_mount(root, nullptr, &error);
385 if (error || !actual_mount) {
386 g_error_free(error);
387 invalid_enclosing_mount = true;
388 } else if (actual_mount) {
389 g_object_unref(actual_mount);
390 }
391
392 // Query the filesystem info for size, free space, and type
393 error = nullptr;
394 GFileInfo* info = g_file_query_filesystem_info(
395 root, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE
396 "," G_FILE_ATTRIBUTE_FILESYSTEM_FREE "," G_FILE_ATTRIBUTE_FILESYSTEM_TYPE,
397 nullptr, &error);
398 if (error) {
399 qLog(Warning) << QString::fromLocal8Bit(error->message);
400 g_error_free(error);
401 } else {
402 filesystem_size = g_file_info_get_attribute_uint64(
403 info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE);
404 filesystem_free = g_file_info_get_attribute_uint64(
405 info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE);
406 filesystem_type = QString::fromUtf8(g_file_info_get_attribute_string(
407 info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE));
408 g_object_unref(info);
409 }
410
411 // Query the file's info for a filesystem ID
412 // Only afc devices (that I know of) give reliably unique IDs
413 if (filesystem_type == "afc") {
414 error = nullptr;
415 info = g_file_query_info(root, G_FILE_ATTRIBUTE_ID_FILESYSTEM,
416 G_FILE_QUERY_INFO_NONE, nullptr, &error);
417 if (error) {
418 qLog(Warning) << QString::fromLocal8Bit(error->message);
419 g_error_free(error);
420 } else {
421 mount_uuid = QString::fromUtf8(g_file_info_get_attribute_string(
422 info, G_FILE_ATTRIBUTE_ID_FILESYSTEM));
423 g_object_unref(info);
424 }
425 }
426
427 g_object_unref(root);
428 }
429
ReadVolumeInfo(GVolume * volume)430 void GioLister::DeviceInfo::ReadVolumeInfo(GVolume* volume) {
431 this->volume.reset_without_add(volume);
432 if (!volume) return;
433
434 volume_name = ConvertAndFree(g_volume_get_name(volume));
435 volume_uuid = ConvertAndFree(g_volume_get_uuid(volume));
436 volume_unix_device = ConvertAndFree(
437 g_volume_get_identifier(volume, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE));
438
439 GFile* root = g_volume_get_activation_root(volume);
440 if (root) {
441 volume_root_uri = g_file_get_uri(root);
442 g_object_unref(root);
443 }
444 }
445
ReadDriveInfo(GDrive * drive)446 void GioLister::DeviceInfo::ReadDriveInfo(GDrive* drive) {
447 this->drive.reset_without_add(drive);
448 if (!drive) return;
449
450 drive_name = ConvertAndFree(g_drive_get_name(drive));
451 drive_removable = g_drive_is_media_removable(drive);
452 }
453
FindUniqueIdByMount(GMount * mount) const454 QString GioLister::FindUniqueIdByMount(GMount* mount) const {
455 for (const DeviceInfo& info : devices_) {
456 if (info.mount == mount) return info.unique_id();
457 }
458 return QString();
459 }
460
FindUniqueIdByVolume(GVolume * volume) const461 QString GioLister::FindUniqueIdByVolume(GVolume* volume) const {
462 for (const DeviceInfo& info : devices_) {
463 if (info.volume == volume) return info.unique_id();
464 }
465 return QString();
466 }
467
VolumeEjectFinished(GObject * object,GAsyncResult * result,gpointer)468 void GioLister::VolumeEjectFinished(GObject* object, GAsyncResult* result,
469 gpointer) {
470 OperationFinished<GVolume>(
471 std::bind(g_volume_eject_with_operation_finish, _1, _2, _3), object,
472 result);
473 }
474
MountEjectFinished(GObject * object,GAsyncResult * result,gpointer)475 void GioLister::MountEjectFinished(GObject* object, GAsyncResult* result,
476 gpointer) {
477 OperationFinished<GMount>(
478 std::bind(g_mount_eject_with_operation_finish, _1, _2, _3), object,
479 result);
480 }
481
MountUnmountFinished(GObject * object,GAsyncResult * result,gpointer)482 void GioLister::MountUnmountFinished(GObject* object, GAsyncResult* result,
483 gpointer) {
484 OperationFinished<GMount>(
485 std::bind(g_mount_unmount_with_operation_finish, _1, _2, _3), object,
486 result);
487 }
488
UnmountDevice(const QString & id)489 void GioLister::UnmountDevice(const QString& id) {
490 QMutexLocker l(&mutex_);
491 if (!devices_.contains(id)) return;
492
493 const DeviceInfo& info = devices_[id];
494
495 if (!info.mount) return;
496
497 if (info.volume) {
498 if (g_volume_can_eject(info.volume)) {
499 g_volume_eject_with_operation(
500 info.volume, G_MOUNT_UNMOUNT_NONE, nullptr, nullptr,
501 (GAsyncReadyCallback)VolumeEjectFinished, nullptr);
502 g_object_unref(info.volume);
503 return;
504 }
505 }
506
507 if (g_mount_can_eject(info.mount)) {
508 g_mount_eject_with_operation(
509 info.mount, G_MOUNT_UNMOUNT_NONE, nullptr, nullptr,
510 (GAsyncReadyCallback)MountEjectFinished, nullptr);
511 } else if (g_mount_can_unmount(info.mount)) {
512 g_mount_unmount_with_operation(
513 info.mount, G_MOUNT_UNMOUNT_NONE, nullptr, nullptr,
514 (GAsyncReadyCallback)MountUnmountFinished, nullptr);
515 }
516 }
517
UpdateDeviceFreeSpace(const QString & id)518 void GioLister::UpdateDeviceFreeSpace(const QString& id) {
519 {
520 QMutexLocker l(&mutex_);
521 if (!devices_.contains(id)) return;
522
523 DeviceInfo& device_info = devices_[id];
524
525 GFile* root = g_mount_get_root(device_info.mount);
526
527 GError* error = nullptr;
528 GFileInfo* info = g_file_query_filesystem_info(
529 root, G_FILE_ATTRIBUTE_FILESYSTEM_FREE, nullptr, &error);
530 if (error) {
531 qLog(Warning) << QString::fromLocal8Bit(error->message);
532 g_error_free(error);
533 } else {
534 device_info.filesystem_free = g_file_info_get_attribute_uint64(
535 info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE);
536 g_object_unref(info);
537 }
538
539 g_object_unref(root);
540 }
541
542 emit DeviceChanged(id);
543 }
544
DeviceNeedsMount(const QString & id)545 bool GioLister::DeviceNeedsMount(const QString& id) {
546 QMutexLocker l(&mutex_);
547 return devices_.contains(id) && !devices_[id].mount;
548 }
549
MountDevice(const QString & id)550 int GioLister::MountDevice(const QString& id) {
551 const int request_id = next_mount_request_id_++;
552 metaObject()->invokeMethod(this, "DoMountDevice", Qt::QueuedConnection,
553 Q_ARG(QString, id), Q_ARG(int, request_id));
554 return request_id;
555 }
556
DoMountDevice(const QString & id,int request_id)557 void GioLister::DoMountDevice(const QString& id, int request_id) {
558 QMutexLocker l(&mutex_);
559 if (!devices_.contains(id)) {
560 emit DeviceMounted(id, request_id, false);
561 return;
562 }
563
564 const DeviceInfo& info = devices_[id];
565 if (info.mount) {
566 // Already mounted
567 emit DeviceMounted(id, request_id, true);
568 return;
569 }
570
571 g_volume_mount(info.volume, G_MOUNT_MOUNT_NONE, nullptr, nullptr,
572 VolumeMountFinished, nullptr);
573 emit DeviceMounted(id, request_id, true);
574 }
575