2011-04-06 13:48:02 +04:00
/*
2014-01-13 19:16:19 +04:00
* Copyright ( C ) by Klaas Freitag < freitag @ owncloud . com >
2011-04-06 13:48:02 +04:00
*
* 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
2016-10-25 12:00:07 +03:00
* the Free Software Foundation ; either version 2 of the License , or
* ( at your option ) any later version .
2011-04-06 13:48:02 +04:00
*
* 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 .
*/
2011-02-17 02:21:45 +03:00
2011-03-16 16:50:34 +03:00
// event masks
2014-07-11 02:31:24 +04:00
# include "folderwatcher.h"
2012-05-21 18:48:49 +04:00
2023-04-07 22:09:06 +03:00
# include "accountstate.h"
# include "account.h"
# include "capabilities.h"
2020-08-13 14:00:56 +03:00
# include <cstdint>
2011-03-16 16:50:34 +03:00
2011-02-17 02:21:45 +03:00
# include <QFileInfo>
# include <QFlags>
# include <QDir>
# include <QMutexLocker>
# include <QStringList>
2011-03-18 15:54:32 +03:00
# include <QTimer>
2011-02-17 02:21:45 +03:00
2014-01-27 15:31:54 +04:00
# if defined(Q_OS_WIN)
2014-07-11 02:31:24 +04:00
# include "folderwatcher_win.h"
2014-01-27 15:31:54 +04:00
# elif defined(Q_OS_MAC)
2014-07-11 02:31:24 +04:00
# include "folderwatcher_mac.h"
2014-04-29 13:02:44 +04:00
# elif defined(Q_OS_UNIX)
2014-07-11 02:31:24 +04:00
# include "folderwatcher_linux.h"
2014-01-27 15:31:54 +04:00
# endif
2015-10-02 10:40:44 +03:00
# include "folder.h"
2019-06-13 10:59:01 +03:00
# include "filesystem.h"
2014-01-27 15:31:54 +04:00
2023-04-07 22:09:06 +03:00
namespace
{
const char * lockFilePatterns [ ] = { " .~lock. " , " ~$ " } ;
}
2014-11-10 00:34:07 +03:00
namespace OCC {
2011-02-17 02:21:45 +03:00
2017-12-28 22:33:10 +03:00
Q_LOGGING_CATEGORY ( lcFolderWatcher , " nextcloud.gui.folderwatcher " , QtInfoMsg )
2017-05-09 15:24:11 +03:00
2018-04-24 10:52:15 +03:00
FolderWatcher : : FolderWatcher ( Folder * folder )
2015-10-02 10:40:44 +03:00
: QObject ( folder )
, _folder ( folder )
2011-02-17 02:21:45 +03:00
{
2023-04-07 22:09:06 +03:00
if ( _folder & & _folder - > accountState ( ) & & _folder - > accountState ( ) - > account ( ) ) {
connect ( _folder - > accountState ( ) - > account ( ) . data ( ) , & Account : : capabilitiesChanged , this , & FolderWatcher : : folderAccountCapabilitiesChanged ) ;
folderAccountCapabilitiesChanged ( ) ;
}
2011-03-21 00:18:38 +03:00
}
2020-05-25 22:33:24 +03:00
FolderWatcher : : ~ FolderWatcher ( ) = default ;
2014-01-27 15:31:54 +04:00
2018-04-24 10:52:15 +03:00
void FolderWatcher : : init ( const QString & root )
{
_d . reset ( new FolderWatcherPrivate ( this , root ) ) ;
_timer . start ( ) ;
}
2023-07-02 20:05:37 +03:00
bool FolderWatcher : : pathIsIgnored ( const QString & path ) const
2014-01-13 18:23:38 +04:00
{
2023-07-02 20:05:37 +03:00
return path . isEmpty ( ) ;
2014-01-13 18:23:38 +04:00
}
2017-10-09 13:06:57 +03:00
bool FolderWatcher : : isReliable ( ) const
{
return _isReliable ;
}
2019-05-09 11:05:49 +03:00
void FolderWatcher : : appendSubPaths ( QDir dir , QStringList & subPaths ) {
QStringList newSubPaths = dir . entryList ( QDir : : NoDotAndDotDot | QDir : : Dirs | QDir : : Files ) ;
for ( int i = 0 ; i < newSubPaths . size ( ) ; i + + ) {
QString path = dir . path ( ) + " / " + newSubPaths [ i ] ;
QFileInfo fileInfo ( path ) ;
subPaths . append ( path ) ;
if ( fileInfo . isDir ( ) ) {
QDir dir ( path ) ;
appendSubPaths ( dir , subPaths ) ;
}
}
}
2019-06-13 10:59:01 +03:00
void FolderWatcher : : startNotificatonTest ( const QString & path )
{
2019-07-16 15:23:15 +03:00
# ifdef Q_OS_MAC
// Testing the folder watcher on OSX is harder because the watcher
// automatically discards changes that were performed by our process.
// It would still be useful to test but the OSX implementation
// is deferred until later.
return ;
# endif
2019-06-13 10:59:01 +03:00
Q_ASSERT ( _testNotificationPath . isEmpty ( ) ) ;
_testNotificationPath = path ;
2019-07-11 14:31:54 +03:00
// Don't do the local file modification immediately:
// wait for FolderWatchPrivate::_ready
startNotificationTestWhenReady ( ) ;
}
void FolderWatcher : : startNotificationTestWhenReady ( )
{
if ( ! _d - > _ready ) {
QTimer : : singleShot ( 1000 , this , & FolderWatcher : : startNotificationTestWhenReady ) ;
return ;
}
auto path = _testNotificationPath ;
2019-06-13 10:59:01 +03:00
if ( QFile : : exists ( path ) ) {
auto mtime = FileSystem : : getModTime ( path ) ;
FileSystem : : setModTime ( path , mtime + 1 ) ;
} else {
QFile f ( path ) ;
f . open ( QIODevice : : WriteOnly | QIODevice : : Append ) ;
}
2023-05-24 17:37:45 +03:00
FileSystem : : setFileHidden ( path , true ) ;
2019-06-13 10:59:01 +03:00
2019-07-11 14:31:54 +03:00
QTimer : : singleShot ( 5000 , this , [ this ] ( ) {
2019-06-13 10:59:01 +03:00
if ( ! _testNotificationPath . isEmpty ( ) )
emit becameUnreliable ( tr ( " The watcher did not receive a test notification. " ) ) ;
_testNotificationPath . clear ( ) ;
} ) ;
}
2019-07-11 14:31:54 +03:00
2019-03-05 15:20:09 +03:00
int FolderWatcher : : testLinuxWatchCount ( ) const
{
# ifdef Q_OS_LINUX
return _d - > testWatchCount ( ) ;
# else
return - 1 ;
# endif
}
2014-01-11 23:35:58 +04:00
void FolderWatcher : : changeDetected ( const QString & path )
2011-04-05 14:16:24 +04:00
{
2019-05-09 11:05:49 +03:00
QFileInfo fileInfo ( path ) ;
2014-02-18 03:36:41 +04:00
QStringList paths ( path ) ;
2019-05-09 11:05:49 +03:00
if ( fileInfo . isDir ( ) ) {
QDir dir ( path ) ;
appendSubPaths ( dir , paths ) ;
}
2014-02-18 03:36:41 +04:00
changeDetected ( paths ) ;
}
void FolderWatcher : : changeDetected ( const QStringList & paths )
{
// TODO: this shortcut doesn't look very reliable:
// - why is the timeout only 1 second?
2015-10-05 07:21:19 +03:00
// - what if there is more than one file being updated frequently?
// - why do we skip the file altogether instead of e.g. reducing the upload frequency?
2014-02-18 03:36:41 +04:00
2014-01-13 18:23:38 +04:00
// Check if the same path was reported within the last second.
2014-02-18 03:36:41 +04:00
QSet < QString > pathsSet = paths . toSet ( ) ;
if ( pathsSet = = _lastPaths & & _timer . elapsed ( ) < 1000 ) {
2014-01-13 18:23:38 +04:00
// the same path was reported within the last second. Skip.
return ;
}
2014-02-18 03:36:41 +04:00
_lastPaths = pathsSet ;
2014-01-13 18:23:38 +04:00
_timer . restart ( ) ;
2014-11-07 12:53:41 +03:00
QSet < QString > changedPaths ;
2023-04-07 22:09:06 +03:00
QSet < QString > unlockedFiles ;
2014-02-18 03:36:41 +04:00
for ( int i = 0 ; i < paths . size ( ) ; + + i ) {
QString path = paths [ i ] ;
2019-06-13 10:59:01 +03:00
if ( ! _testNotificationPath . isEmpty ( )
& & Utility : : fileNamesEqual ( path , _testNotificationPath ) ) {
_testNotificationPath . clear ( ) ;
}
2023-04-07 22:09:06 +03:00
if ( _shouldWatchForFileUnlocking ) {
const auto unlockedFilePath = possiblyAddUnlockedFilePath ( path ) ;
if ( ! unlockedFilePath . isEmpty ( ) ) {
unlockedFiles . insert ( unlockedFilePath ) ;
}
qCDebug ( lcFolderWatcher ) < < " Unlocked files: " < < unlockedFiles . values ( ) ;
}
// ------- handle ignores:
2014-02-18 03:36:41 +04:00
if ( pathIsIgnored ( path ) ) {
continue ;
}
2014-11-07 12:53:41 +03:00
changedPaths . insert ( path ) ;
2014-02-18 03:36:41 +04:00
}
2023-04-07 22:09:06 +03:00
if ( ! unlockedFiles . isEmpty ( ) ) {
emit filesLockReleased ( unlockedFiles ) ;
}
2014-11-07 12:53:41 +03:00
if ( changedPaths . isEmpty ( ) ) {
2014-01-13 18:23:38 +04:00
return ;
}
2017-03-30 14:46:20 +03:00
qCInfo ( lcFolderWatcher ) < < " Detected changes in paths: " < < changedPaths ;
2014-11-07 12:53:41 +03:00
foreach ( const QString & path , changedPaths ) {
emit pathChanged ( path ) ;
2014-02-18 03:36:41 +04:00
}
2012-12-05 21:30:40 +04:00
}
2023-04-07 22:09:06 +03:00
void FolderWatcher : : folderAccountCapabilitiesChanged ( )
{
_shouldWatchForFileUnlocking = _folder - > accountState ( ) - > account ( ) - > capabilities ( ) . filesLockAvailable ( ) ;
}
QString FolderWatcher : : possiblyAddUnlockedFilePath ( const QString & path )
{
qCDebug ( lcFolderWatcher ) < < " Checking if it is a lock file: " < < path ;
const auto pathSplit = path . split ( QLatin1Char ( ' / ' ) , Qt : : SkipEmptyParts ) ;
if ( pathSplit . isEmpty ( ) ) {
return { } ;
}
QString lockFilePatternFound ;
for ( const auto & lockFilePattern : lockFilePatterns ) {
if ( pathSplit . last ( ) . startsWith ( lockFilePattern ) ) {
lockFilePatternFound = lockFilePattern ;
break ;
}
}
if ( lockFilePatternFound . isEmpty ( ) | | QFileInfo : : exists ( path ) ) {
return { } ;
}
qCDebug ( lcFolderWatcher ) < < " Found a lock file with prefix: " < < lockFilePatternFound < < " in path: " < < path ;
const auto lockFilePathWitoutPrefix = QString ( path ) . replace ( lockFilePatternFound , QStringLiteral ( " " ) ) ;
auto lockFilePathWithoutPrefixSplit = lockFilePathWitoutPrefix . split ( QLatin1Char ( ' . ' ) ) ;
if ( lockFilePathWithoutPrefixSplit . size ( ) < 2 ) {
return { } ;
}
auto extensionSanitized = lockFilePathWithoutPrefixSplit . takeLast ( ) . toStdString ( ) ;
// remove possible non-alphabetical characters at the end of the extension
extensionSanitized . erase (
std : : remove_if ( extensionSanitized . begin ( ) , extensionSanitized . end ( ) , [ ] ( const auto & ch ) {
return ! std : : isalnum ( ch ) ;
} ) ,
extensionSanitized . end ( )
) ;
lockFilePathWithoutPrefixSplit . push_back ( QString : : fromStdString ( extensionSanitized ) ) ;
auto unlockedFilePath = lockFilePathWithoutPrefixSplit . join ( QLatin1Char ( ' . ' ) ) ;
if ( ! QFile : : exists ( unlockedFilePath ) ) {
unlockedFilePath . clear ( ) ;
qCDebug ( lcFolderWatcher ) < < " Assumed unlocked file path " < < unlockedFilePath < < " does not exist. Going to try to find matching file " ;
auto splitFilePath = unlockedFilePath . split ( QLatin1Char ( ' / ' ) ) ;
if ( splitFilePath . size ( ) > 1 ) {
const auto lockFileName = splitFilePath . takeLast ( ) ;
// some software will modify lock file name such that it does not correspond to original file (removing some symbols from the name, so we will search for a matching file
unlockedFilePath = findMatchingUnlockedFileInDir ( splitFilePath . join ( QLatin1Char ( ' / ' ) ) , lockFileName ) ;
}
}
if ( unlockedFilePath . isEmpty ( ) | | ! QFile : : exists ( unlockedFilePath ) ) {
return { } ;
}
return unlockedFilePath ;
}
QString FolderWatcher : : findMatchingUnlockedFileInDir ( const QString & dirPath , const QString & lockFileName )
{
QString foundFilePath ;
const QDir dir ( dirPath ) ;
for ( const auto & candidateUnlockedFileInfo : dir . entryInfoList ( QDir : : Files ) ) {
if ( candidateUnlockedFileInfo . fileName ( ) . contains ( lockFileName ) ) {
foundFilePath = candidateUnlockedFileInfo . absoluteFilePath ( ) ;
break ;
}
}
return foundFilePath ;
}
2014-11-10 00:34:07 +03:00
} // namespace OCC