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