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