/*
* orphanedfilesdialog.cc
*
* Copyright (c) 2002, 2003 Frerich Raabe <raabe@kde.org>
*
* 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. For licensing and distribution details, check the
* accompanying file 'COPYING'.
*/
#include "databasewrapper.h"
#include "orphanedfilesdialog.h"
#include "portmgr.h"
#include <kapplication.h>
#include <kfiledetailview.h>
#include <kiconloader.h>
#include <klocale.h>
#include <kmessagebox.h>
#include <kopenwith.h>
#include <kpropertiesdialog.h>
#include <ksqueezedtextlabel.h>
#include <qheader.h>
#include <qpopupmenu.h>
#include <qthread.h>
#include <qvbox.h>
#include <sys/types.h>
#include <dirent.h>
#include <math.h>
#include <unistd.h>
namespace
{
bool isPrime( int number )
{
if ( number <= 1 )
return false;
int divisor = int( sqrt( number ) );
while ( number % divisor != 0 )
--divisor;
return divisor == 1;
}
int nextPrime( int number )
{
int i = number;
while ( !isPrime( i ) )
++i;
return i;
}
}
using namespace Barry;
class DirChangeEvent : public QCustomEvent
{
public:
static const int Id = 10001;
DirChangeEvent( const QString &dir ) : QCustomEvent( Id ), m_dir( dir ) { }
QString dir() const { return m_dir; }
private:
QString m_dir;
};
class FoundFileEvent : public QCustomEvent
{
public:
static const int Id = 10002;
FoundFileEvent( const QString &name ) : QCustomEvent( Id ), m_name( name ) { }
QString name() const { return m_name; }
private:
QString m_name;
};
class TraverserDoneEvent : public QCustomEvent
{
public:
static const int Id = 10003;
TraverserDoneEvent() : QCustomEvent( Id ) { }
};
class TraverserListEvent : public QCustomEvent
{
public:
static const int Id = 10004;
TraverserListEvent( const QPtrList<QCustomEvent> &eventList )
: QCustomEvent( Id ), m_eventList( eventList ) { }
const QPtrList<QCustomEvent> &eventList() const { return m_eventList; }
private:
QPtrList<QCustomEvent> m_eventList;
};
class DirTraverserThread : public QThread
{
public:
DirTraverserThread( QWidget *parent, const QStringList &startDirs,
const QDict<bool> &ignoredFiles );
void abort() { m_abort = true; }
protected:
virtual void run();
private:
void scanDir( const QString &path );
QWidget *m_parent;
QStringList m_startDirs;
QDict<bool> m_ignoredFiles;
bool m_abort;
QPtrList<QCustomEvent> m_eventList;
QTime m_timeSnapshot;
};
DirTraverserThread::DirTraverserThread( QWidget *parent,
const QStringList &startDirs,
const QDict<bool> &ignoredFiles )
: m_parent( parent ),
m_startDirs( startDirs ),
m_ignoredFiles( ignoredFiles ),
m_abort( false )
{
}
void DirTraverserThread::run()
{
m_timeSnapshot = QTime::currentTime();
QStringList::ConstIterator it = m_startDirs.begin();
QStringList::ConstIterator end = m_startDirs.end();
for ( ; it != end; ++it )
scanDir( *it );
kapp->postEvent( m_parent, new TraverserDoneEvent );
}
void DirTraverserThread::scanDir( const QString &path )
{
if ( m_abort )
return;
if ( m_ignoredFiles.find( path ) != 0 )
return;
DIR *dirp = opendir( path.latin1() );
if ( !dirp )
return;
m_eventList.append( new DirChangeEvent( path ) );
struct dirent *dp;
while ( ( dp = readdir( dirp ) ) != 0 ) {
if ( dp->d_type == DT_DIR && strcmp( dp->d_name, "." ) != 0 && strcmp( dp->d_name, ".." ) != 0 ) {
const QString absPath = path + QString::fromLatin1( "/" ) + QString::fromLatin1( dp->d_name, dp->d_namlen );
if ( access( absPath.latin1(), R_OK | X_OK ) == 0 )
scanDir( absPath );
} else if ( dp->d_type == DT_REG ) {
QDir d( path );
const QString canonPath = d.canonicalPath();
const QString absName = canonPath + QString::fromLatin1( "/" ) + QString::fromLatin1( dp->d_name, dp->d_namlen );
if ( m_ignoredFiles.find( absName ) == 0 )
m_eventList.append( new FoundFileEvent( absName ) );
}
if ( m_timeSnapshot.msecsTo( QTime::currentTime() ) > 100 ) {
kapp->postEvent( m_parent, new TraverserListEvent( m_eventList ) );
m_eventList.clear();
m_timeSnapshot = QTime::currentTime();
}
if ( m_abort )
break;
}
closedir( dirp );
}
OrphanedFilesDialog::OrphanedFilesDialog( QWidget *parent, const char *name )
: KDialogBase( parent, name, true, i18n( "Searching for orphaned files..." ),
Cancel, Cancel, true )
{
resize( 500, 300 );
QVBox *mainWidget = makeVBoxMainWidget();
m_statusText = new KSqueezedTextLabel(
i18n( "Scanning: %1" ).arg( QString::null ),
mainWidget, "m_statusText"
);
m_foundFiles = new KFileDetailView( mainWidget, "m_foundFiles" );
m_foundFiles->setSorting( QDir::Unsorted );
m_foundFiles->hide();
// Disable clicking to work around a crash in KFileDetailView
m_foundFiles->header()->setClickEnabled( false );
connect( m_foundFiles, SIGNAL( contextMenu( KListView *, QListViewItem *, const QPoint & ) ),
this, SLOT( showContextMenu( KListView *, QListViewItem *, const QPoint & ) ) );
}
void OrphanedFilesDialog::show()
{
KDialogBase::show();
search();
}
QStringList OrphanedFilesDialog::determineRegisteredFiles()
{
QStringList registeredFiles;
/* We know that all the pkgdb can map a filename to a package, so we
* get all keys and strip those which don't start with a slash, that
* should be the list of registered files.
*/
registeredFiles = PortMgr::self().pkgDb().getAllKeys();
/* Since ~95% of the keys in the pkgdb are filenames, and they come first,
* we start searching from the end and just strip all non-filename
* keys.
*/
QStringList::Iterator it = registeredFiles.end();
--it;
while ( ( *it )[ 0 ] != '/' && it != registeredFiles.begin() )
--it;
registeredFiles.erase( ++it, registeredFiles.end() );
return registeredFiles;
}
QStringList OrphanedFilesDialog::determineIgnoredFiles()
{
KConfig *cfg = kapp->config();
cfg->setGroup( QString::fromLatin1( "Orphaned files" ) );
return cfg->readListEntry( "Ignored files" );
}
void OrphanedFilesDialog::search()
{
/* Merge the lists of registered and ignored files into a QDict so that
* the directory traverser thread wastes less time with lookups.
*/
const QStringList allFiles = determineRegisteredFiles() +
determineIgnoredFiles();
QDict<bool> ignoredDict( nextPrime( allFiles.count() ) );
ignoredDict.setAutoDelete( true );
QStringList::ConstIterator it = allFiles.begin();
QStringList::ConstIterator end = allFiles.end();
for ( ; it != end; ++it ) {
QString s = *it;
if ( s.at( s.length() - 1 ) == '/' )
s.remove( s.length() - 1, 1 );
ignoredDict.insert( s, new bool( true ) );
}
m_traverserThread = new DirTraverserThread( this,
PortMgr::self().prefixes(),
ignoredDict );
m_traverserThread->start();
}
void OrphanedFilesDialog::slotCancel()
{
if ( m_traverserThread ) {
m_statusText->setText( i18n( "Aborting..." ) );
m_statusText->update();
m_traverserThread->abort();
m_traverserThread->wait( 5 );
delete m_traverserThread;
}
reject();
}
void OrphanedFilesDialog::showContextMenu( KListView *, QListViewItem *, const QPoint &p )
{
QPopupMenu *menu = new QPopupMenu( this );
menu->insertItem( SmallIcon( QString::fromLatin1( "misc" ) ),
i18n( "Properties..." ), this, SLOT( showProperties() ) );
menu->insertItem( SmallIcon( QString::fromLatin1( "fileopen" ) ),
i18n( "Open..." ), this, SLOT( openFile() ) );
menu->insertSeparator();
menu->insertItem( SmallIcon( QString::fromLatin1( "editdelete" ) ),
i18n( "Delete..." ),
this, SLOT( deleteFile() ) );
menu->insertSeparator();
menu->insertItem( i18n( "Add file to ignore list" ),
this, SLOT( addFileToIgnoreList() ) );
menu->insertItem( i18n( "Add directory to ignore list" ),
this, SLOT( addDirectoryToIgnoreList() ) );
menu->exec( p );
}
void OrphanedFilesDialog::showProperties()
{
new KPropertiesDialog( m_foundFiles->currentFileItem()->url() );
}
void OrphanedFilesDialog::openFile()
{
const KURL fileURL = m_foundFiles->currentFileItem()->url();
const KMimeType::Ptr mime = KMimeType::findByURL( fileURL.url(), 0, true );
/* application shouldn't get passed to KRun since that will execute the
* respective file. Instead, we open a "Open with" dialog to let the user
* decide (most likely, an editor will be chosen). */
if ( mime->name().startsWith( QString::fromLatin1( "application/" ) ) ) {
KFileOpenWithHandler *openWithHandler = new KFileOpenWithHandler;
openWithHandler->displayOpenWithDialog( fileURL );
} else {
new KRun( fileURL );
}
}
void OrphanedFilesDialog::deleteFile()
{
const QString fileName = m_foundFiles->currentFileItem()->url().prettyURL( -1, KURL::StripFileProtocol );
if ( KMessageBox::warningYesNo( this, i18n( "Are you sure you want to erase the "
" file %1?" ).arg( fileName ),
i18n( "Confirmation" ), KStdGuiItem::yes(), KStdGuiItem::no(),
QString::fromLatin1( "showDeleteConfirmationBox" ) ) == KMessageBox::No )
return;
const QFileInfo fileInfo( fileName );
if ( !fileInfo.isWritable() ) {
KMessageBox::error( this, i18n( "Cannot delete file %1, no write "
"access." ).arg( fileName ), i18n( "Cannot delete file" ) );
return;
}
if ( !QFile::remove( fileName ) )
KMessageBox::error( this, i18n( "Unknown error occured while trying "
"to delete file %1!" ).arg( fileName ), i18n( "Couldn't delete file" ) );
return;
}
void OrphanedFilesDialog::addFileToIgnoreList()
{
addItemToIgnoreList( m_foundFiles->currentFileItem()->url().prettyURL( -1, KURL::StripFileProtocol ) );
}
void OrphanedFilesDialog::addDirectoryToIgnoreList()
{
addItemToIgnoreList( m_foundFiles->currentFileItem()->url().directory() );
}
void OrphanedFilesDialog::addItemToIgnoreList( const QString &item )
{
QStringList ignoredFiles = determineIgnoredFiles();
ignoredFiles << item;
KConfig *cfg = kapp->config();
cfg->setGroup( QString::fromLatin1( "Orphaned files" ) );
cfg->writeEntry( "Ignored files", ignoredFiles );
cfg->sync();
}
void OrphanedFilesDialog::customEvent( QCustomEvent *e )
{
if ( e->type() == DirChangeEvent::Id ) {
DirChangeEvent *de = static_cast<DirChangeEvent *>( e );
m_statusText->setText( i18n( "Scanning: %1" ).arg( de->dir() ) );
} else if ( e->type() == FoundFileEvent::Id ) {
FoundFileEvent *fe = static_cast<FoundFileEvent *>( e );
KFileItem *fi = new KFileItem( KFileItem::Unknown, KFileItem::Unknown,
KURL( fe->name() ) );
KFileListViewItem *item = new KFileListViewItem( m_foundFiles, fi );
item->setText( 0, fe->name() );
} else if ( e->type() == TraverserDoneEvent::Id ) {
m_foundFiles->show();
m_statusText->setText( i18n( "Search finished." ) );
} else if ( e->type() == TraverserListEvent::Id ) {
TraverserListEvent *le = static_cast<TraverserListEvent *>( e );
QPtrList<QCustomEvent> events = le->eventList();
for ( QCustomEvent *event = events.first(); event != 0; event = events.next() ) {
customEvent( event );
delete event;
}
}
}
#include "orphanedfilesdialog.moc"
// vim:ts=4:sw=4:noet:list
syntax highlighted by Code2HTML, v. 0.9.1