/* * Copyright (C) by Klaas Freitag * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ #include "mirall/folderman.h" #include "mirall/mirallconfigfile.h" #include "mirall/unisonfolder.h" #include "mirall/csyncfolder.h" #include "mirall/owncloudfolder.h" #include "mirall/syncresult.h" #include "mirall/inotify.h" #include "mirall/theme.h" #ifdef Q_OS_MAC #include #endif #ifdef Q_OS_WIN #include #endif #include #include namespace Mirall { FolderMan::FolderMan(QObject *parent) : QObject(parent) { // if QDir::mkpath would not be so stupid, I would not need to have this // duplication of folderConfigPath() here QDir storageDir(QDesktopServices::storageLocation(QDesktopServices::DataLocation)); storageDir.mkpath(QLatin1String("folders")); _folderConfigPath = QDesktopServices::storageLocation(QDesktopServices::DataLocation) + QLatin1String("/folders"); _folderChangeSignalMapper = new QSignalMapper(this); connect(_folderChangeSignalMapper, SIGNAL(mapped(const QString &)), this, SIGNAL(folderSyncStateChange(const QString &))); } FolderMan::~FolderMan() { foreach (Folder *folder, _folderMap) { delete folder; } } Mirall::Folder::Map FolderMan::map() { return _folderMap; } int FolderMan::setupFolders() { // setup a handler to look for configuration changes #ifdef CHECK_FOR_SETUP_CHANGES _configFolderWatcher = new FolderWatcher( _folderConfigPath ); _configFolderWatcher->setEventInterval(20000); connect(_configFolderWatcher, SIGNAL(folderChanged(const QStringList &)), this, SLOT( slotReparseConfiguration()) ); #endif int cnt = setupKnownFolders(); return cnt; } void FolderMan::slotReparseConfiguration() { setupKnownFolders(); } int FolderMan::setupKnownFolders() { qDebug() << "* Setup folders from " << _folderConfigPath; _folderMap.clear(); // FIXME: check if delete of folder structure happens QDir dir( _folderConfigPath ); dir.setFilter(QDir::Files); QStringList list = dir.entryList(); foreach ( const QString& alias, list ) { Folder *f = setupFolderFromConfigFile( alias ); if( f ) { emit( folderSyncStateChange( f->alias() ) ); } } // return the number of valid folders. return _folderMap.size(); } #define SLASH_TAG QLatin1String("__SLASH__") #define BSLASH_TAG QLatin1String("__BSLASH__") #define QMARK_TAG QLatin1String("__QMARK__") #define PERCENT_TAG QLatin1String("__PERCENT__") #define STAR_TAG QLatin1String("__STAR__") #define COLON_TAG QLatin1String("__COLON__") #define PIPE_TAG QLatin1String("__PIPE__") #define QUOTE_TAG QLatin1String("__QUOTE__") #define LT_TAG QLatin1String("__LESS_THAN__") #define GT_TAG QLatin1String("__GREATER_THAN__") #define PAR_O_TAG QLatin1String("__PAR_OPEN__") #define PAR_C_TAG QLatin1String("__PAR_CLOSE__") QString FolderMan::escapeAlias( const QString& alias ) const { QString a(alias); a.replace( QLatin1Char('/'), SLASH_TAG ); a.replace( QLatin1Char('\\'), BSLASH_TAG ); a.replace( QLatin1Char('?'), QMARK_TAG ); a.replace( QLatin1Char('%'), PERCENT_TAG ); a.replace( QLatin1Char('*'), STAR_TAG ); a.replace( QLatin1Char(':'), COLON_TAG ); a.replace( QLatin1Char('|'), PIPE_TAG ); a.replace( QLatin1Char('"'), QUOTE_TAG ); a.replace( QLatin1Char('<'), LT_TAG ); a.replace( QLatin1Char('>'), GT_TAG ); a.replace( QLatin1Char('['), PAR_O_TAG ); a.replace( QLatin1Char(']'), PAR_C_TAG ); return a; } QString FolderMan::unescapeAlias( const QString& alias ) const { QString a(alias); a.replace( SLASH_TAG, QLatin1String("/") ); a.replace( BSLASH_TAG, QLatin1String("\\") ); a.replace( QMARK_TAG, QLatin1String("?") ); a.replace( PERCENT_TAG, QLatin1String("%") ); a.replace( STAR_TAG, QLatin1String("*") ); a.replace( COLON_TAG, QLatin1String(":") ); a.replace( PIPE_TAG, QLatin1String("|") ); a.replace( QUOTE_TAG, QLatin1String("\"") ); a.replace( LT_TAG, QLatin1String("<") ); a.replace( GT_TAG, QLatin1String(">") ); a.replace( PAR_O_TAG, QLatin1String("[") ); a.replace( PAR_C_TAG, QLatin1String("]") ); return a; } void FolderMan::setupFavLink(const QString &folder) { #ifdef Q_OS_WIN // Windows Explorer: Place under "Favorites" (Links) wchar_t path[MAX_PATH]; SHGetSpecialFolderPath(0, path, CSIDL_PROFILE, FALSE); QString profile = QDir::fromNativeSeparators(QString::fromWCharArray(path)); QString folderName = QDir::fromNativeSeparators(folder.mid(folder.lastIndexOf('/'))); QString linkName = profile+QLatin1String("/Links/") + folderName + QLatin1String(".lnk"); if (!QFile::link(folder, linkName)) qDebug() << Q_FUNC_INFO << "linking" << folder << "to" << linkName << "failed!"; #elif defined (Q_OS_MAC) // Finder: Place under "Places" QString folderUrl = QUrl::fromLocalFile(folder).toString(); CFStringRef folderCFStr = CFStringCreateWithCharacters(0, reinterpret_cast(folderUrl.unicode()), folder.length()); CFURLRef urlRef = CFURLCreateWithString(NULL, folderCFStr, 0); LSSharedFileListRef placesItems = LSSharedFileListCreate(0, kLSSharedFileListFavoriteItems, 0); if (placesItems) { //Insert an item to the list. LSSharedFileListItemRef item = LSSharedFileListInsertItemURL(placesItems, kLSSharedFileListItemBeforeFirst, 0, 0, urlRef, 0, 0); if (item) CFRelease(item); } CFRelease(placesItems); CFRelease(folderCFStr); CFRelease(urlRef); #endif } // filename is the name of the file only, it does not include // the configuration directory path Folder* FolderMan::setupFolderFromConfigFile(const QString &file) { Folder *folder = 0; qDebug() << " ` -> setting up:" << file; QString escapedAlias(file); // check the unescaped variant (for the case the filename comes out // of the directory listing. If the file is not existing, escape the // file and try again. QFileInfo cfgFile( _folderConfigPath, file); if( !cfgFile.exists() ) { // try the escaped variant. escapedAlias = escapeAlias(file); cfgFile.setFile( _folderConfigPath, escapedAlias ); } if( !cfgFile.isReadable() ) { qDebug() << "Can not read folder definition for alias " << cfgFile.filePath(); return folder; } QSettings settings( cfgFile.filePath(), QSettings::IniFormat); qDebug() << " -> file path: " << settings.fileName(); // Check if the filename is equal to the group setting. If not, use the group // name as an alias. QStringList groups = settings.childGroups(); if( ! groups.contains(escapedAlias) && groups.count() > 0 ) { escapedAlias = groups.first(); } settings.beginGroup( escapedAlias ); // read the group with the same name as the file which is the folder alias QString path = settings.value(QLatin1String("localpath")).toString(); QString backend = settings.value(QLatin1String("backend")).toString(); QString targetPath = settings.value( QLatin1String("targetPath") ).toString(); // QString connection = settings.value( QLatin1String("connection") ).toString(); QString alias = unescapeAlias( escapedAlias ); if (!backend.isEmpty()) { if (backend == QLatin1String("unison")) { folder = new UnisonFolder(alias, path, targetPath, this ); } else if (backend == QLatin1String("csync")) { #ifdef WITH_CSYNC folder = new CSyncFolder(alias, path, targetPath, this ); #else qCritical() << "* csync support not enabled!! ignoring:" << file; #endif } else if( backend == QLatin1String("owncloud") ) { #ifdef WITH_CSYNC MirallConfigFile cfgFile; // assemble the owncloud url to pass to csync, incl. webdav QString oCUrl = cfgFile.ownCloudUrl( QString::null, true ); // cut off the leading slash, oCUrl always has a trailing. if( targetPath.startsWith(QLatin1Char('/')) ) { targetPath.remove(0,1); } folder = new ownCloudFolder( alias, path, oCUrl + targetPath, this ); folder->setConfigFile(file); #else qCritical() << "* owncloud support not enabled!! ignoring:" << file; #endif } else { qWarning() << "unknown backend" << backend; return NULL; } } if( folder ) { folder->setBackend( backend ); // folder->setOnlyOnlineEnabled(settings.value("folder/onlyOnline", false).toBool()); folder->setOnlyThisLANEnabled(settings.value(QLatin1String("folder/onlyThisLAN"), false).toBool()); _folderMap[alias] = folder; qDebug() << "Adding folder to Folder Map " << folder; /* Use a signal mapper to connect the signals to the alias */ connect(folder, SIGNAL(scheduleToSync(const QString&)), SLOT(slotScheduleSync(const QString&))); connect(folder, SIGNAL(syncStateChange()), _folderChangeSignalMapper, SLOT(map())); connect(folder, SIGNAL(syncStarted()), SLOT(slotFolderSyncStarted())); connect(folder, SIGNAL(syncFinished(SyncResult)), SLOT(slotFolderSyncFinished(SyncResult))); _folderChangeSignalMapper->setMapping( folder, folder->alias() ); } return folder; } void FolderMan::disableFoldersWithRestore() { _folderEnabledMap.clear(); foreach( Folder *f, _folderMap ) { // store the enabled state, then make sure it is disabled _folderEnabledMap.insert(f->alias(), f->syncEnabled()); f->setSyncEnabled(false); } } void FolderMan::restoreEnabledFolders() { foreach( Folder *f, _folderMap ) { if (_folderEnabledMap.contains( f->alias() )) { f->setSyncEnabled( _folderEnabledMap.value( f->alias() ) ); } } } void FolderMan::slotEnableFolder( const QString& alias, bool enable ) { if( ! _folderMap.contains( alias ) ) { qDebug() << "!! Can not enable alias " << alias << ", can not be found in folderMap."; return; } Folder *f = _folderMap[alias]; if( f ) { f->setSyncEnabled(enable); } } // this really terminates, ie. no questions, no prisoners. // csync still remains in a stable state, regardless of that. void FolderMan::terminateSyncProcess( const QString& alias ) { Folder *f = _folderMap[alias]; if( f ) { f->slotTerminateSync(); } } Folder *FolderMan::folder( const QString& alias ) { if( !alias.isEmpty() ) { if( _folderMap.contains( alias )) { return _folderMap[alias]; } } return 0; } SyncResult FolderMan::syncResult( const QString& alias ) { SyncResult res; Folder *f = folder( alias ); if( f ) { res = f->syncResult(); } return res; } /* * if a folder wants to be synced, it calls this slot and is added * to the queue. The slot to actually start a sync is called afterwards. */ void FolderMan::slotScheduleSync( const QString& alias ) { if( alias.isEmpty() ) return; qDebug() << "Schedule folder " << alias << " to sync!"; if( _currentSyncFolder == alias ) { // the current folder is currently syncing. } if( _scheduleQueue.contains( alias ) ) { qDebug() << " II> Sync for folder " << alias << " already scheduled, do not enqueue!"; } else { _scheduleQueue.append( alias ); slotScheduleFolderSync(); } } /* * slot to start folder syncs. * It is either called from the slot where folders enqueue themselves for * syncing or after a folder sync was finished. */ void FolderMan::slotScheduleFolderSync() { if( !_currentSyncFolder.isEmpty() ) { qDebug() << "Currently folder " << _currentSyncFolder << " is running, wait for finish!"; return; } qDebug() << "XX slotScheduleFolderSync: folderQueue size: " << _scheduleQueue.count(); if( ! _scheduleQueue.isEmpty() ) { const QString alias = _scheduleQueue.takeFirst(); if( _folderMap.contains( alias ) ) { Folder *f = _folderMap[alias]; _currentSyncFolder = alias; f->startSync( QStringList() ); } } } void FolderMan::slotFolderSyncStarted( ) { qDebug() << ">===================================== sync started for " << _currentSyncFolder; } /* * a folder indicates that its syncing is finished. * Start the next sync after the system had some milliseconds to breath. */ void FolderMan::slotFolderSyncFinished( const SyncResult& ) { qDebug() << "<===================================== sync finished for " << _currentSyncFolder; _currentSyncFolder.clear(); QTimer::singleShot(200, this, SLOT(slotScheduleFolderSync())); } /** * Add a folder definition to the config * Params: * QString backend * QString alias * QString sourceFolder on local machine * QString targetPath on remote * bool onlyThisLAN, currently unused. */ void FolderMan::addFolderDefinition( const QString& backend, const QString& alias, const QString& sourceFolder, const QString& targetPath, bool onlyThisLAN ) { QString escapedAlias = escapeAlias(alias); // Create a settings file named after the alias QSettings settings( _folderConfigPath + QLatin1Char('/') + escapedAlias, QSettings::IniFormat); settings.setValue(QString::fromLatin1("%1/localPath").arg(escapedAlias), sourceFolder ); settings.setValue(QString::fromLatin1("%1/targetPath").arg(escapedAlias), targetPath ); settings.setValue(QString::fromLatin1("%1/backend").arg(escapedAlias), backend ); settings.setValue(QString::fromLatin1("%1/connection").arg(escapedAlias), Theme::instance()->appName()); settings.setValue(QString::fromLatin1("%1/onlyThisLAN").arg(escapedAlias), onlyThisLAN ); settings.sync(); setupFavLink(sourceFolder); } void FolderMan::removeAllFolderDefinitions() { foreach( Folder *f, _folderMap.values() ) { slotRemoveFolder( f->alias() ); } // clear the queue. _scheduleQueue.clear(); } void FolderMan::slotRemoveFolder( const QString& alias ) { if( alias.isEmpty() ) return; if( _currentSyncFolder == alias ) { // terminate if the sync is currently underway. terminateSyncProcess( alias ); } removeFolder(alias); } // remove a folder from the map. Should be sure n void FolderMan::removeFolder( const QString& alias ) { Folder *f = 0; if( _folderMap.contains( alias )) { qDebug() << "Removing " << alias; f = _folderMap.take( alias ); f->wipe(); } else { qDebug() << "!! Can not remove " << alias << ", not in folderMap."; } if( f ) { QFile file( _folderConfigPath + QLatin1Char('/') + f->configFile() ); if( file.exists() ) { qDebug() << "Remove folder config file " << file.fileName(); file.remove(); } f->deleteLater(); } } }