1 /****************************************************************************************
2  * Copyright (c) 2006-2007 Maximilian Kossick <maximilian.kossick@googlemail.com>       *
3  *                                                                                      *
4  * This program is free software; you can redistribute it and/or modify it under        *
5  * the terms of the GNU General Public License as published by the Free Software        *
6  * Foundation; either version 2 of the License, or (at your option) any later           *
7  * version.                                                                             *
8  *                                                                                      *
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
11  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
12  *                                                                                      *
13  * You should have received a copy of the GNU General Public License along with         *
14  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
15  ****************************************************************************************/
16 
17 #define DEBUG_PREFIX "MountPointManager"
18 
19 #include "MountPointManager.h"
20 
21 #include "MediaDeviceCache.h"
22 #include "core/support/Amarok.h"
23 #include "core/support/Debug.h"
24 #include <core/storage/SqlStorage.h>
25 #include "core-impl/collections/db/sql/device/massstorage/MassStorageDeviceHandler.h"
26 #include "core-impl/collections/db/sql/device/nfs/NfsDeviceHandler.h"
27 #include "core-impl/collections/db/sql/device/smb/SmbDeviceHandler.h"
28 
29 #include <KConfigGroup>
30 #include <Solid/Predicate>
31 #include <Solid/Device>
32 
33 #include <QDesktopServices>
34 #include <QDir>
35 #include <QFile>
36 #include <QList>
37 #include <QStringList>
38 #include <QTimer>
39 
MountPointManager(QObject * parent,QSharedPointer<SqlStorage> storage)40 MountPointManager::MountPointManager( QObject *parent, QSharedPointer<SqlStorage> storage )
41     : QObject( parent )
42     , m_storage( storage )
43     , m_ready( false )
44 {
45     DEBUG_BLOCK
46     setObjectName( "MountPointManager" );
47 
48     if ( !Amarok::config( "Collection" ).readEntry( "DynamicCollection", true ) )
49     {
50         debug() << "Dynamic Collection deactivated in amarokrc, not loading plugins, not connecting signals";
51         m_ready = true;
52         handleMusicLocation();
53         return;
54     }
55 
56     connect( MediaDeviceCache::instance(), &MediaDeviceCache::deviceAdded, this, &MountPointManager::slotDeviceAdded );
57     connect( MediaDeviceCache::instance(), &MediaDeviceCache::deviceRemoved, this, &MountPointManager::slotDeviceRemoved );
58 
59     createDeviceFactories();
60 }
61 
62 void
handleMusicLocation()63 MountPointManager::handleMusicLocation()
64 {
65     // For users who were using QDesktopServices::MusicLocation exclusively up
66     // to v2.2.2, which did not store the location into config.
67     // and also for versions up to 2.7-git that did write the Use MusicLocation entry
68 
69     KConfigGroup folders = Amarok::config( "Collection Folders" );
70     const QString entryKey( "Use MusicLocation" );
71     if( !folders.hasKey( entryKey ) )
72         return; // good, already solved, nothing to do
73 
74     // write the music location as another collection folder in this case
75     if( folders.readEntry( entryKey, false ) )
76     {
77         const QUrl musicUrl = QUrl::fromLocalFile( QStandardPaths::writableLocation( QStandardPaths::MusicLocation ) );
78         const QString musicDir = musicUrl.adjusted( QUrl::StripTrailingSlash ).toLocalFile();
79         const QDir dir( musicDir );
80         if( dir.exists() && dir.isReadable() )
81         {
82             QStringList currentFolders = collectionFolders();
83             if( !currentFolders.contains( musicDir ) )
84                 setCollectionFolders( currentFolders << musicDir );
85         }
86     }
87 
88     folders.deleteEntry( entryKey ); // get rid of it for good
89 }
90 
~MountPointManager()91 MountPointManager::~MountPointManager()
92 {
93     DEBUG_BLOCK
94 
95     m_handlerMapMutex.lock();
96     foreach( DeviceHandler *dh, m_handlerMap )
97         delete dh;
98     m_handlerMapMutex.unlock();
99 
100     // DeviceHandlerFactories are memory managed using QObject parentship
101 }
102 
103 
104 void
createDeviceFactories()105 MountPointManager::createDeviceFactories()
106 {
107     DEBUG_BLOCK
108     QList<DeviceHandlerFactory*> factories;
109     factories << new MassStorageDeviceHandlerFactory( this );
110     factories << new NfsDeviceHandlerFactory( this );
111     factories << new SmbDeviceHandlerFactory( this );
112     foreach( DeviceHandlerFactory *factory, factories )
113     {
114         debug() << "Initializing DeviceHandlerFactory of type:" << factory->type();
115         if( factory->canCreateFromMedium() )
116             m_mediumFactories.append( factory );
117         else if (factory->canCreateFromConfig() )
118             m_remoteFactories.append( factory );
119         else //FIXME max: better error message
120             debug() << "Unknown DeviceHandlerFactory";
121     }
122 
123     Solid::Predicate predicate = Solid::Predicate( Solid::DeviceInterface::StorageAccess );
124     QList<Solid::Device> devices = Solid::Device::listFromQuery( predicate );
125     foreach( const Solid::Device &device, devices )
126         createHandlerFromDevice( device, device.udi() );
127 
128     m_ready = true;
129     handleMusicLocation();
130 }
131 
132 int
getIdForUrl(const QUrl & url)133 MountPointManager::getIdForUrl( const QUrl &url )
134 {
135     int mountPointLength = 0;
136     int id = -1;
137     m_handlerMapMutex.lock();
138     foreach( DeviceHandler *dh, m_handlerMap )
139     {
140         if ( url.path().startsWith( dh->getDevicePath() ) && mountPointLength < dh->getDevicePath().length() )
141         {
142             id = m_handlerMap.key( dh );
143             mountPointLength = dh->getDevicePath().length();
144         }
145     }
146     m_handlerMapMutex.unlock();
147     if ( mountPointLength > 0 )
148     {
149         return id;
150     }
151     else
152     {
153         //default fallback if we could not identify the mount point.
154         //treat -1 as mount point / in all other methods
155         return -1;
156     }
157 }
158 
159 bool
isMounted(const int deviceId) const160 MountPointManager::isMounted( const int deviceId ) const
161 {
162     m_handlerMapMutex.lock();
163     const bool result = m_handlerMap.contains( deviceId );
164     m_handlerMapMutex.unlock();
165     return result;
166 }
167 
168 QString
getMountPointForId(const int id) const169 MountPointManager::getMountPointForId( const int id ) const
170 {
171     QString mountPoint;
172     if ( isMounted( id ) )
173     {
174         m_handlerMapMutex.lock();
175         mountPoint = m_handlerMap[id]->getDevicePath();
176         m_handlerMapMutex.unlock();
177     }
178     else
179         //TODO better error handling
180         mountPoint = '/';
181     return mountPoint;
182 }
183 
184 QString
getAbsolutePath(const int deviceId,const QString & relativePath) const185 MountPointManager::getAbsolutePath( const int deviceId, const QString& relativePath ) const
186 {
187     if( QDir( relativePath ).isAbsolute() )
188     {
189         //debug() << "relativePath is already absolute";
190         return relativePath;
191     }
192 
193     QUrl rurl = QUrl::fromLocalFile( relativePath );
194     QUrl absoluteUrl = QUrl::fromLocalFile( QDir::rootPath() );
195 
196     //debug() << "id is " << deviceId << ", relative path is " << relativePath;
197     if ( deviceId == -1 )
198     {
199         absoluteUrl.setPath( QDir::rootPath() + relativePath );
200         absoluteUrl.setPath( QDir::cleanPath( absoluteUrl.path() ) );
201         //debug() << "Deviceid is -1, using relative Path as absolute Path, returning " << absoluteUrl.toLocalFile();
202     }
203     else
204     {
205         m_handlerMapMutex.lock();
206         if ( m_handlerMap.contains( deviceId ) )
207         {
208             m_handlerMap[deviceId]->getURL( absoluteUrl, rurl );
209             m_handlerMapMutex.unlock();
210         }
211         else
212         {
213             m_handlerMapMutex.unlock();
214             const QStringList lastMountPoint = m_storage->query( QString( "SELECT lastmountpoint FROM devices WHERE id = %1" )
215                                                                  .arg( deviceId ) );
216             if ( lastMountPoint.isEmpty() )
217             {
218                 //hmm, no device with that id in the DB...serious problem
219                 warning() << "Device " << deviceId << " not in database, this should never happen!";
220                 return getAbsolutePath( -1, relativePath );
221             }
222             else
223             {
224                 absoluteUrl = QUrl::fromLocalFile( lastMountPoint.first() );
225                 absoluteUrl = absoluteUrl.adjusted(QUrl::StripTrailingSlash);
226                 absoluteUrl.setPath( absoluteUrl.path() + QLatin1Char('/') + rurl.path() );
227                 absoluteUrl.setPath( QDir::cleanPath( absoluteUrl.path() ) );
228                 //debug() << "Device " << deviceId << " not mounted, using last mount point and returning " << absoluteUrl.toLocalFile();
229             }
230         }
231     }
232 
233     if( QFileInfo( absoluteUrl.toLocalFile() ).isDir() )
234         absoluteUrl.setPath( absoluteUrl.adjusted( QUrl::StripTrailingSlash ).path() + '/' );
235 
236     return absoluteUrl.toLocalFile();
237 }
238 
239 QString
getRelativePath(const int deviceId,const QString & absolutePath) const240 MountPointManager::getRelativePath( const int deviceId, const QString& absolutePath ) const
241 {
242     DEBUG_BLOCK
243 
244     debug() << absolutePath;
245 
246     QMutexLocker locker(&m_handlerMapMutex);
247     if ( deviceId != -1 && m_handlerMap.contains( deviceId ) )
248     {
249         //FIXME max: returns garbage if the absolute path is actually not under the device's mount point
250         return QDir( m_handlerMap[deviceId]->getDevicePath() ).relativeFilePath( absolutePath );
251     }
252     else
253     {
254         //TODO: better error handling
255 #ifdef Q_OS_WIN32
256         return QUrl( absolutePath ).toLocalFile();
257 #else
258         return QDir::root().relativeFilePath(absolutePath);
259 #endif
260     }
261 }
262 
263 IdList
getMountedDeviceIds() const264 MountPointManager::getMountedDeviceIds() const
265 {
266     m_handlerMapMutex.lock();
267     IdList list( m_handlerMap.keys() );
268     m_handlerMapMutex.unlock();
269     list.append( -1 );
270     return list;
271 }
272 
273 QStringList
collectionFolders() const274 MountPointManager::collectionFolders() const
275 {
276     if( !m_ready )
277     {
278         debug() << "requested collectionFolders from MountPointManager that is not yet ready";
279         return QStringList();
280     }
281 
282     //TODO max: cache data
283     QStringList result;
284     KConfigGroup folders = Amarok::config( "Collection Folders" );
285     const IdList ids = getMountedDeviceIds();
286 
287     foreach( int id, ids )
288     {
289         const QStringList rpaths = folders.readEntry( QString::number( id ), QStringList() );
290         foreach( const QString &strIt, rpaths )
291         {
292             const QUrl url = QUrl::fromLocalFile( ( strIt == "./" ) ? getMountPointForId( id ) : getAbsolutePath( id, strIt ) );
293             const QString absPath = url.adjusted(QUrl::StripTrailingSlash).toLocalFile();
294             if ( !result.contains( absPath ) )
295                 result.append( absPath );
296         }
297     }
298 
299     return result;
300 }
301 
302 void
setCollectionFolders(const QStringList & folders)303 MountPointManager::setCollectionFolders( const QStringList &folders )
304 {
305     typedef QMap<int, QStringList> FolderMap;
306     KConfigGroup folderConf = Amarok::config( "Collection Folders" );
307     FolderMap folderMap;
308 
309     for( const QString &folder : folders )
310     {
311         int id = getIdForUrl( QUrl::fromLocalFile(folder) );
312         const QString rpath = getRelativePath( id, folder );
313         if( folderMap.contains( id ) ) {
314             if( !folderMap[id].contains( rpath ) )
315                 folderMap[id].append( rpath );
316         }
317         else
318             folderMap[id] = QStringList( rpath );
319     }
320     //make sure that collection folders on devices which are not in foldermap are deleted
321     const IdList ids = getMountedDeviceIds();
322     for ( int deviceId : ids )
323     {
324         if( !folderMap.contains( deviceId ) )
325         {
326             folderConf.deleteEntry( QString::number( deviceId ) );
327         }
328     }
329     QMapIterator<int, QStringList> i( folderMap );
330     while( i.hasNext() )
331     {
332         i.next();
333         folderConf.writeEntry( QString::number( i.key() ), i.value() );
334     }
335 }
336 
337 void
slotDeviceAdded(const QString & udi)338 MountPointManager::slotDeviceAdded( const QString &udi )
339 {
340     DEBUG_BLOCK
341     Solid::Predicate predicate = Solid::Predicate( Solid::DeviceInterface::StorageAccess );
342     QList<Solid::Device> devices = Solid::Device::listFromQuery( predicate );
343     //Looking for a specific udi in predicate seems flaky/buggy; the foreach loop barely
344     //takes any time, so just be safe
345     bool found = false;
346     debug() << "looking for udi " << udi;
347     foreach( const Solid::Device &device, devices )
348     {
349         if( device.udi() == udi )
350         {
351             createHandlerFromDevice( device, udi );
352             found = true;
353         }
354     }
355     if( !found )
356         debug() << "Did not find device from Solid for udi " << udi;
357 }
358 
359 void
slotDeviceRemoved(const QString & udi)360 MountPointManager::slotDeviceRemoved( const QString &udi )
361 {
362     DEBUG_BLOCK
363     m_handlerMapMutex.lock();
364     foreach( DeviceHandler *dh, m_handlerMap )
365     {
366         if( dh->deviceMatchesUdi( udi ) )
367         {
368             int key = m_handlerMap.key( dh );
369             m_handlerMap.remove( key );
370             delete dh;
371             debug() << "removed device " << key;
372             m_handlerMapMutex.unlock();
373             //we found the medium which was removed, so we can abort the loop
374             Q_EMIT deviceRemoved( key );
375             return;
376         }
377     }
378     m_handlerMapMutex.unlock();
379 }
380 
createHandlerFromDevice(const Solid::Device & device,const QString & udi)381 void MountPointManager::createHandlerFromDevice( const Solid::Device& device, const QString &udi )
382 {
383     DEBUG_BLOCK
384     if ( device.isValid() )
385     {
386         debug() << "Device added and mounted, checking handlers";
387         foreach( DeviceHandlerFactory *factory, m_mediumFactories )
388         {
389             if( factory->canHandle( device ) )
390             {
391                 debug() << "found handler for " << udi;
392                 DeviceHandler *handler = factory->createHandler( device, udi, m_storage );
393                 if( !handler )
394                 {
395                     debug() << "Factory " << factory->type() << "could not create device handler";
396                     break;
397                 }
398                 int key = handler->getDeviceID();
399                 m_handlerMapMutex.lock();
400                 if( m_handlerMap.contains( key ) )
401                 {
402                     debug() << "Key " << key << " already exists in handlerMap, replacing";
403                     delete m_handlerMap[key];
404                     m_handlerMap.remove( key );
405                 }
406                 m_handlerMap.insert( key, handler );
407                 m_handlerMapMutex.unlock();
408 //                 debug() << "added device " << key << " with mount point " << volumeAccess->mountPoint();
409                 Q_EMIT deviceAdded( key );
410                 break;  //we found the added medium and don't have to check the other device handlers
411             }
412             else
413                 debug() << "Factory can't handle device " << udi;
414         }
415     }
416     else
417         debug() << "Device not valid!";
418 }
419