File indexing completed on 2024-05-05 04:49:51
0001 /*************************************************************************** 0002 * Copyright (C) 2003-2005 Max Howell <max.howell@methylblue.com> * 0003 * (C) 2003-2010 Mark Kretschmann <kretschmann@kde.org> * 0004 * (C) 2005-2007 Alexandre Oliveira <aleprj@gmail.com> * 0005 * (C) 2008 Dan Meltzer <parallelgrapefruit@gmail.com> * 0006 * (C) 2008-2009 Jeff Mitchell <mitchell@kde.org> * 0007 * * 0008 * This program is free software; you can redistribute it and/or modify * 0009 * it under the terms of the GNU General Public License as published by * 0010 * the Free Software Foundation; either version 2 of the License, or * 0011 * (at your option) any later version. * 0012 * * 0013 * This program is distributed in the hope that it will be useful, * 0014 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0016 * GNU General Public License for more details. * 0017 * * 0018 * You should have received a copy of the GNU General Public License * 0019 * along with this program; if not, write to the * 0020 * Free Software Foundation, Inc., * 0021 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * 0022 ***************************************************************************/ 0023 0024 #include "CollectionScanner.h" 0025 0026 #include "Version.h" // for AMAROK_VERSION 0027 #include "collectionscanner/BatchFile.h" 0028 #include "collectionscanner/Directory.h" 0029 #include "collectionscanner/Track.h" 0030 0031 #include <QTimer> 0032 #include <QThread> 0033 0034 #include <QString> 0035 #include <QStringList> 0036 #include <QDir> 0037 #include <QFile> 0038 #include <QDateTime> 0039 #include <QXmlStreamReader> 0040 #include <QXmlStreamWriter> 0041 #include <QSharedMemory> 0042 #include <QByteArray> 0043 #include <QTextStream> 0044 #include <QDataStream> 0045 #include <QBuffer> 0046 #include <QDebug> 0047 0048 #include <algorithm> 0049 0050 #ifdef Q_OS_LINUX 0051 // for ioprio 0052 #include <unistd.h> 0053 #include <sys/syscall.h> 0054 enum { 0055 IOPRIO_CLASS_NONE, 0056 IOPRIO_CLASS_RT, 0057 IOPRIO_CLASS_BE, 0058 IOPRIO_CLASS_IDLE 0059 }; 0060 0061 enum { 0062 IOPRIO_WHO_PROCESS = 1, 0063 IOPRIO_WHO_PGRP, 0064 IOPRIO_WHO_USER 0065 }; 0066 #define IOPRIO_CLASS_SHIFT 13 0067 #endif 0068 0069 0070 int 0071 main( int argc, char *argv[] ) 0072 { 0073 CollectionScanner::Scanner scanner( argc, argv ); 0074 return scanner.exec(); 0075 } 0076 0077 CollectionScanner::Scanner::Scanner( int &argc, char **argv ) 0078 : QCoreApplication( argc, argv ) 0079 , m_charset( false ) 0080 , m_newerTime(0) 0081 , m_incremental( false ) 0082 , m_recursively( false ) 0083 , m_restart( false ) 0084 , m_idlePriority( false ) 0085 { 0086 setObjectName( QStringLiteral("amarokcollectionscanner") ); 0087 0088 readArgs(); 0089 0090 if( m_idlePriority ) 0091 { 0092 bool ioPriorityWorked = false; 0093 #if defined(Q_OS_LINUX) && defined(SYS_ioprio_set) 0094 // try setting the idle priority class 0095 ioPriorityWorked = ( syscall( SYS_ioprio_set, IOPRIO_WHO_PROCESS, 0, 0096 IOPRIO_CLASS_IDLE << IOPRIO_CLASS_SHIFT ) >= 0 ); 0097 // try setting the lowest priority in the best-effort priority class (the default class) 0098 if( !ioPriorityWorked ) 0099 ioPriorityWorked = ( syscall( SYS_ioprio_set, IOPRIO_WHO_PROCESS, 0, 0100 7 | ( IOPRIO_CLASS_BE << IOPRIO_CLASS_SHIFT ) ) >= 0 ); 0101 #endif 0102 if( !ioPriorityWorked && QThread::currentThread() ) 0103 QThread::currentThread()->setPriority( QThread::IdlePriority ); 0104 } 0105 } 0106 0107 0108 CollectionScanner::Scanner::~Scanner() 0109 { 0110 } 0111 0112 void 0113 CollectionScanner::Scanner::readBatchFile( const QString &path ) 0114 { 0115 QFile batchFile( path ); 0116 0117 if( !batchFile.exists() ) 0118 error( tr( "File \"%1\" not found." ).arg( path ) ); 0119 0120 if( !batchFile.open( QIODevice::ReadOnly ) ) 0121 error( tr( "Could not open file \"%1\"." ).arg( path ) ); 0122 0123 BatchFile batch( path ); 0124 foreach( const QString &str, batch.directories() ) 0125 { 0126 m_folders.append( str ); 0127 } 0128 0129 foreach( const CollectionScanner::BatchFile::TimeDefinition &def, batch.timeDefinitions() ) 0130 { 0131 m_mTimes.insert( def.first, def.second ); 0132 } 0133 } 0134 0135 void 0136 CollectionScanner::Scanner::readNewerTime( const QString &path ) 0137 { 0138 QFileInfo file( path ); 0139 0140 if( !file.exists() ) 0141 error( tr( "File \"%1\" not found." ).arg( path ) ); 0142 0143 m_newerTime = qMax<qint64>( m_newerTime, file.lastModified().toSecsSinceEpoch() ); 0144 } 0145 0146 0147 void 0148 CollectionScanner::Scanner::doJob() //SLOT 0149 { 0150 QFile xmlFile; 0151 xmlFile.open( stdout, QIODevice::WriteOnly ); 0152 QXmlStreamWriter xmlWriter( &xmlFile ); 0153 xmlWriter.setAutoFormatting( true ); 0154 0155 // get a list of folders to scan. We do it even if resuming because we don't want 0156 // to save the (perhaps very big) list of directories into shared memory, bug 327812 0157 QStringList entries; 0158 { 0159 QSet<QString> entriesSet; 0160 0161 foreach( QString dir, m_folders ) // krazy:exclude=foreach 0162 { 0163 if( dir.isEmpty() ) 0164 //apparently somewhere empty strings get into the mix 0165 //which results in a full-system scan! Which we can't allow 0166 continue; 0167 0168 // Make sure that all paths are absolute, not relative 0169 if( QDir::isRelativePath( dir ) ) 0170 dir = QDir::cleanPath( QDir::currentPath() + QLatin1Char('/') + dir ); 0171 0172 if( !dir.endsWith( QLatin1Char('/') ) ) 0173 dir += '/'; 0174 0175 addDir( dir, &entriesSet ); // checks m_recursively 0176 } 0177 0178 entries = entriesSet.values(); 0179 std::sort( entries.begin(), entries.end() ); // the sort is crucial because of restarts and lastDirectory handling 0180 } 0181 0182 if( m_restart ) 0183 { 0184 m_scanningState.readFull(); 0185 QString lastEntry = m_scanningState.lastDirectory(); 0186 0187 int index = entries.indexOf( lastEntry ); 0188 if( index >= 0 ) 0189 // strip already processed entries, but *keep* the lastEntry 0190 entries = entries.mid( index ); 0191 else 0192 qWarning() << Q_FUNC_INFO << "restarting scan after a crash, but lastDirectory" 0193 << lastEntry << "not found in folders to scan (size" << entries.size() 0194 << "). Starting scanning from the beginning."; 0195 } 0196 else // first attempt 0197 { 0198 m_scanningState.writeFull(); // just trigger write to initialise memory 0199 0200 xmlWriter.writeStartDocument(); 0201 xmlWriter.writeStartElement(QStringLiteral("scanner")); 0202 xmlWriter.writeAttribute(QStringLiteral("count"), QString::number( entries.count() ) ); 0203 if( m_incremental ) 0204 xmlWriter.writeAttribute(QStringLiteral("incremental"), QString()); 0205 // write some information into the file and close previous tag 0206 xmlWriter.writeComment("Created by amarokcollectionscanner " AMAROK_VERSION " on "+QDateTime::currentDateTime().toString()); 0207 xmlFile.flush(); 0208 } 0209 0210 // --- now do the scanning 0211 foreach( const QString &path, entries ) 0212 { 0213 CollectionScanner::Directory dir( path, &m_scanningState, 0214 m_incremental && !isModified( path ) ); 0215 0216 xmlWriter.writeStartElement( QStringLiteral("directory") ); 0217 dir.toXml( &xmlWriter ); 0218 xmlWriter.writeEndElement(); 0219 xmlFile.flush(); 0220 } 0221 0222 // --- write the end element (must be done by hand as we might not have written the start element when restarting) 0223 xmlFile.write("\n</scanner>\n"); 0224 0225 quit(); 0226 } 0227 0228 void 0229 CollectionScanner::Scanner::addDir( const QString& dir, QSet<QString>* entries ) 0230 { 0231 // Linux specific, but this fits the 90% rule 0232 if( dir.startsWith( QLatin1String("/dev") ) || dir.startsWith( QLatin1String("/sys") ) || dir.startsWith( QLatin1String("/proc") ) ) 0233 return; 0234 0235 if( entries->contains( dir ) ) 0236 return; 0237 0238 QDir d( dir ); 0239 if( !d.exists() ) 0240 { 0241 QTextStream stream( stderr ); 0242 stream << "Directory \""<<dir<<"\" does not exist." << Qt::endl; 0243 return; 0244 } 0245 0246 entries->insert( dir ); 0247 0248 if( !m_recursively ) 0249 return; // finished 0250 0251 d.setFilter( QDir::NoDotAndDotDot | QDir::Dirs ); 0252 const QFileInfoList fileInfos = d.entryInfoList(); 0253 0254 for ( const QFileInfo &fi : fileInfos ) 0255 { 0256 if( !fi.exists() ) 0257 continue; 0258 0259 const QFileInfo &f = fi.isSymLink() ? QFileInfo( fi.symLinkTarget() ) : fi; 0260 0261 if( !f.exists() ) 0262 continue; 0263 0264 if( f.isDir() ) 0265 { 0266 addDir( QString( f.absoluteFilePath() + '/' ), entries ); 0267 } 0268 } 0269 } 0270 0271 bool 0272 CollectionScanner::Scanner::isModified( const QString& dir ) 0273 { 0274 QFileInfo info( dir ); 0275 if( !info.exists() ) 0276 return false; 0277 0278 uint lastModified = info.lastModified().toSecsSinceEpoch(); 0279 0280 if( m_mTimes.contains( dir ) ) 0281 return m_mTimes.value( dir ) != lastModified; 0282 else 0283 return m_newerTime < lastModified; 0284 } 0285 0286 void 0287 CollectionScanner::Scanner::readArgs() 0288 { 0289 QStringList argslist = arguments(); 0290 if( argslist.size() < 2 ) 0291 displayHelp(); 0292 0293 bool missingArg = false; 0294 0295 for( int argnum = 1; argnum < argslist.count(); argnum++ ) 0296 { 0297 QString arg = argslist.at( argnum ); 0298 0299 if( arg.startsWith( QLatin1String("--") ) ) 0300 { 0301 QString myarg = QString( arg ).remove( 0, 2 ); 0302 if( myarg == QLatin1String("newer") ) 0303 { 0304 if( argslist.count() > argnum + 1 ) 0305 readNewerTime( argslist.at( argnum + 1 ) ); 0306 else 0307 missingArg = true; 0308 argnum++; 0309 } 0310 else if( myarg == QLatin1String("batch") ) 0311 { 0312 if( argslist.count() > argnum + 1 ) 0313 readBatchFile( argslist.at( argnum + 1 ) ); 0314 else 0315 missingArg = true; 0316 argnum++; 0317 } 0318 else if( myarg == QLatin1String("sharedmemory") ) 0319 { 0320 if( argslist.count() > argnum + 1 ) 0321 m_scanningState.setKey( argslist.at( argnum + 1 ) ); 0322 else 0323 missingArg = true; 0324 argnum++; 0325 } 0326 else if( myarg == QLatin1String("version") ) 0327 displayVersion(); 0328 else if( myarg == QLatin1String("incremental") ) 0329 m_incremental = true; 0330 else if( myarg == QLatin1String("recursive") ) 0331 m_recursively = true; 0332 else if( myarg == QLatin1String("restart") ) 0333 m_restart = true; 0334 else if( myarg == QLatin1String("idlepriority") ) 0335 m_idlePriority = true; 0336 else if( myarg == QLatin1String("charset") ) 0337 m_charset = true; 0338 else 0339 displayHelp(); 0340 0341 } 0342 else if( arg.startsWith( '-' ) ) 0343 { 0344 QString myarg = QString( arg ).remove( 0, 1 ); 0345 int pos = 0; 0346 while( pos < myarg.length() ) 0347 { 0348 if( myarg[pos] == 'r' ) 0349 m_recursively = true; 0350 else if( myarg[pos] == 'v' ) 0351 displayVersion(); 0352 else if( myarg[pos] == 's' ) 0353 m_restart = true; 0354 else if( myarg[pos] == 'c' ) 0355 m_charset = true; 0356 else if( myarg[pos] == 'i' ) 0357 m_incremental = true; 0358 else 0359 displayHelp(); 0360 0361 ++pos; 0362 } 0363 } 0364 else 0365 { 0366 if( !arg.isEmpty() ) 0367 m_folders.append( arg ); 0368 } 0369 } 0370 0371 if( missingArg ) 0372 displayHelp( tr( "Missing argument for option %1" ).arg( argslist.last() ) ); 0373 0374 0375 CollectionScanner::Track::setUseCharsetDetector( m_charset ); 0376 0377 // Start the actual scanning job 0378 QTimer::singleShot( 0, this, &Scanner::doJob ); 0379 } 0380 0381 void 0382 CollectionScanner::Scanner::error( const QString &str ) 0383 { 0384 QTextStream stream( stderr ); 0385 stream << str << Qt::endl; 0386 stream.flush(); 0387 0388 // Nothing else to do, so we exit directly 0389 ::exit( 0 ); 0390 } 0391 0392 /** This function is called by Amarok to verify that Amarok an Scanner versions match */ 0393 void 0394 CollectionScanner::Scanner::displayVersion() 0395 { 0396 QTextStream stream( stdout ); 0397 stream << AMAROK_VERSION << Qt::endl; 0398 stream.flush(); 0399 0400 // Nothing else to do, so we exit directly 0401 ::exit( 0 ); 0402 } 0403 0404 void 0405 CollectionScanner::Scanner::displayHelp( const QString &error ) 0406 { 0407 QTextStream stream( error.isEmpty() ? stdout : stderr ); 0408 stream << error 0409 << tr( "Amarok Collection Scanner\n" 0410 "Scans directories and outputs a xml file with the results.\n" 0411 "For more information see http://community.kde.org/Amarok/Development/BatchMode\n\n" 0412 "Usage: amarokcollectionscanner [options] <Folder(s)>\n" 0413 "User-modifiable Options:\n" 0414 "<Folder(s)> : list of folders to scan\n" 0415 "-h, --help : This help text\n" 0416 "-v, --version : Print the version of this tool\n" 0417 "-r, --recursive : Scan folders recursively\n" 0418 "-i, --incremental : Incremental scan (modified folders only)\n" 0419 "-s, --restart : After a crash, restart the scanner in its last position\n" 0420 " --idlepriority : Run at idle priority\n" 0421 " --sharedmemory <key> : A shared memory segment to be used for restarting a scan\n" 0422 " --newer <path> : Only scan directories if modification time is newer than <path>\n" 0423 " Only useful in incremental scan mode\n" 0424 " --batch <path> : Add the directories from the batch xml file\n" 0425 " batch file format should look like this:\n" 0426 " <scanner>\n" 0427 " <directory>\n" 0428 " <path>/absolute/path/of/directory</path>\n" 0429 " <mtime>1234</mtime> (this is optional)\n" 0430 " </directory>\n" 0431 " </scanner>\n" 0432 " You can also use a previous scan result for that.\n" 0433 ) 0434 << Qt::endl; 0435 stream.flush(); 0436 0437 ::exit(0); 0438 } 0439 0440