kde-playground/kdepim/mailcommon/job/backupjob.cpp

461 lines
14 KiB
C++
Raw Normal View History

/*
Copyright (c) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
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) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
by the membership of KDE e.V.), which shall act as a proxy
defined in Section 14 of version 3 of the license.
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.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "backupjob.h"
#include <libkdepim/misc/broadcaststatus.h>
#include <Akonadi/CollectionDeleteJob>
#include <Akonadi/CollectionFetchJob>
#include <Akonadi/CollectionFetchScope>
#include <Akonadi/ItemFetchJob>
#include <Akonadi/ItemFetchScope>
#include <KMime/Message>
#include <KLocale>
#include <KMessageBox>
#include <KTar>
#include <KZip>
#include <kio/global.h> //krazy:exclude=camelcase as there is no such
#include <QFileInfo>
#include <QTimer>
using namespace MailCommon;
static const mode_t archivePerms = S_IFREG | 0644;
BackupJob::BackupJob( QWidget *parent )
: QObject( parent ),
mArchiveTime(QDateTime::currentDateTime().toTime_t()),
mArchiveType( Zip ),
mRootFolder( 0 ),
mArchive( 0 ),
mParentWidget( parent ),
mArchivedMessages( 0 ),
mArchivedSize( 0 ),
mProgressItem( 0 ),
mAborted( false ),
mDeleteFoldersAfterCompletion( false ),
mRecursive( true ),
mCurrentFolder( Akonadi::Collection() ),
mCurrentJob( 0 ),
mDisplayMessageBox(true)
{
}
BackupJob::~BackupJob()
{
mPendingFolders.clear();
delete mArchive;
mArchive = 0;
}
void BackupJob::setRootFolder( const Akonadi::Collection &rootFolder )
{
mRootFolder = rootFolder;
}
void BackupJob::setRealPath(const QString &path)
{
mRealPath = path;
}
void BackupJob::setSaveLocation( const KUrl &savePath )
{
mMailArchivePath = savePath;
}
void BackupJob::setArchiveType( ArchiveType type )
{
mArchiveType = type;
}
void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem )
{
mDeleteFoldersAfterCompletion = deleteThem;
}
void BackupJob::setRecursive( bool recursive )
{
mRecursive = recursive;
}
bool BackupJob::queueFolders( const Akonadi::Collection &root )
{
mPendingFolders.append( root );
if ( mRecursive ) {
// FIXME: Get rid of the exec()
// We could do a recursive CollectionFetchJob, but we only fetch the first level
// and then recurse manually. This is needed because a recursive fetch doesn't
// sort the collections the way we want. We need all first level children to be
// in the mPendingFolders list before all second level children, so that the
// directories for the first level are written before the directories in the
// second level, in the archive file.
Akonadi::CollectionFetchJob *job =
new Akonadi::CollectionFetchJob( root, Akonadi::CollectionFetchJob::FirstLevel );
job->fetchScope().setAncestorRetrieval( Akonadi::CollectionFetchScope::All );
job->exec();
if ( job->error() ) {
kWarning() << job->errorString();
abort( i18n( "Unable to retrieve folder list." ) );
return false;
}
foreach ( const Akonadi::Collection &collection, job->collections() ) {
if ( !queueFolders( collection ) ) {
return false;
}
}
}
mAllFolders = mPendingFolders;
return true;
}
bool BackupJob::hasChildren( const Akonadi::Collection &collection ) const
{
foreach ( const Akonadi::Collection &curCol, mAllFolders ) {
if ( collection == curCol.parentCollection() ) {
return true;
}
}
return false;
}
void BackupJob::cancelJob()
{
abort( i18n( "The operation was canceled by the user." ) );
}
void BackupJob::abort( const QString &errorMessage )
{
// We could be called this twice, since killing the current job below will
// cause the job to fail, and that will call abort()
if ( mAborted ) {
return;
}
mAborted = true;
if ( mCurrentFolder.isValid() ) {
mCurrentFolder = Akonadi::Collection();
}
if ( mArchive && mArchive->isOpen() ) {
mArchive->close();
}
if ( mCurrentJob ) {
mCurrentJob->kill();
mCurrentJob = 0;
}
if ( mProgressItem ) {
mProgressItem->setComplete();
mProgressItem = 0;
// The progressmanager will delete it
}
QString text = i18n( "Failed to archive the folder '%1'.", mRootFolder.name() );
text += QLatin1Char('\n') + errorMessage;
Q_EMIT error(text);
if (mDisplayMessageBox)
KMessageBox::sorry( mParentWidget, text, i18n( "Archiving failed" ) );
deleteLater();
// Clean up archive file here?
}
void BackupJob::finish()
{
if ( mArchive->isOpen() ) {
if ( !mArchive->close() ) {
abort( i18n( "Unable to finalize the archive file." ) );
return;
}
}
const QString archivingStr( i18n( "Archiving finished" ) );
KPIM::BroadcastStatus::instance()->setStatusMsg( archivingStr );
if ( mProgressItem ) {
mProgressItem->setStatus( archivingStr );
mProgressItem->setComplete();
mProgressItem = 0;
}
QFileInfo archiveFileInfo( mMailArchivePath.path() );
QString text = i18n( "Archiving folder '%1' successfully completed. "
"The archive was written to the file '%2'.",
mRealPath.isEmpty() ? mRootFolder.name() : mRealPath, mMailArchivePath.path() );
text += QLatin1Char('\n') + i18np( "1 message of size %2 was archived.",
"%1 messages with the total size of %2 were archived.",
mArchivedMessages, KIO::convertSize( mArchivedSize ) );
text += QLatin1Char('\n') + i18n( "The archive file has a size of %1.",
KIO::convertSize( archiveFileInfo.size() ) );
if (mDisplayMessageBox) {
KMessageBox::information( mParentWidget, text, i18n( "Archiving finished" ) );
}
if ( mDeleteFoldersAfterCompletion ) {
// Some safety checks first...
if ( archiveFileInfo.exists() && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) {
// Sorry for any data loss!
new Akonadi::CollectionDeleteJob( mRootFolder );
}
}
Q_EMIT backupDone(text);
deleteLater();
}
void BackupJob::archiveNextMessage()
{
if ( mAborted ) {
return;
}
if ( mPendingMessages.isEmpty() ) {
kDebug() << "===> All messages done in folder " << mCurrentFolder.name();
archiveNextFolder();
return;
}
Akonadi::Item item = mPendingMessages.front();
mPendingMessages.pop_front();
kDebug() << "Fetching item with ID" << item.id() << "for folder" << mCurrentFolder.name();
mCurrentJob = new Akonadi::ItemFetchJob( item );
mCurrentJob->fetchScope().fetchFullPayload( true );
connect( mCurrentJob, SIGNAL(result(KJob*)),
this, SLOT(itemFetchJobResult(KJob*)) );
}
void BackupJob::processMessage( const Akonadi::Item &item )
{
if ( mAborted ) {
return;
}
const KMime::Message::Ptr message = item.payload<KMime::Message::Ptr>();
kDebug() << "Processing message with subject " << message->subject( false );
const QByteArray messageData = message->encodedContent();
const qint64 messageSize = messageData.size();
const QString messageName = QString::number( item.id() );
const QString fileName = pathForCollection( mCurrentFolder ) + QLatin1String("/cur/") + messageName;
// PORT ME: user and group!
kDebug() << "AKONDI PORT: disabled code here!";
if ( !mArchive->writeFile( fileName, QLatin1String("user"), QLatin1String("group"), messageData, messageSize, archivePerms, mArchiveTime, mArchiveTime, mArchiveTime) ) {
abort( i18n( "Failed to write a message into the archive folder '%1'.",
mCurrentFolder.name() ) );
return;
}
++mArchivedMessages;
mArchivedSize += messageSize;
// Use a singleshot timer, otherwise the job started in archiveNextMessage()
// will hang
QTimer::singleShot( 0, this, SLOT(archiveNextMessage()) );
}
void BackupJob::itemFetchJobResult( KJob *job )
{
if ( mAborted ) {
return;
}
Q_ASSERT( job == mCurrentJob );
mCurrentJob = 0;
if ( job->error() ) {
Q_ASSERT( mCurrentFolder.isValid() );
kWarning() << job->errorString();
abort( i18n( "Downloading a message in folder '%1' failed.", mCurrentFolder.name() ) );
} else {
Akonadi::ItemFetchJob *fetchJob = dynamic_cast<Akonadi::ItemFetchJob*>( job );
Q_ASSERT( fetchJob );
Q_ASSERT( fetchJob->items().size() == 1 );
processMessage( fetchJob->items().first() );
}
}
bool BackupJob::writeDirHelper( const QString &directoryPath )
{
// PORT ME: Correct user/group
kDebug() << "AKONDI PORT: Disabled code here!";
return mArchive->writeDir( directoryPath, QLatin1String("user"), QLatin1String("group"), 040755, mArchiveTime, mArchiveTime, mArchiveTime );
}
QString BackupJob::collectionName( const Akonadi::Collection &collection ) const
{
foreach ( const Akonadi::Collection &curCol, mAllFolders ) {
if ( curCol == collection ) {
return curCol.name();
}
}
Q_ASSERT( false );
return QString();
}
QString BackupJob::pathForCollection( const Akonadi::Collection &collection ) const
{
QString fullPath = collectionName( collection );
Akonadi::Collection curCol = collection.parentCollection();
if ( collection != mRootFolder ) {
Q_ASSERT( curCol.isValid() );
while ( curCol != mRootFolder ) {
fullPath.prepend( QLatin1Char('.') + collectionName( curCol ) + QString::fromLatin1(".directory/") );
curCol = curCol.parentCollection();
}
Q_ASSERT( curCol == mRootFolder );
fullPath.prepend( QLatin1Char('.') + collectionName( curCol ) + QString::fromLatin1(".directory/") );
}
return fullPath;
}
QString BackupJob::subdirPathForCollection( const Akonadi::Collection &collection ) const
{
QString path = pathForCollection( collection );
const int parentDirEndIndex = path.lastIndexOf( collection.name() );
Q_ASSERT( parentDirEndIndex != -1 );
path = path.left( parentDirEndIndex );
path.append( QLatin1Char('.') + collection.name() + QLatin1String(".directory") );
return path;
}
void BackupJob::archiveNextFolder()
{
if ( mAborted ) {
return;
}
if ( mPendingFolders.isEmpty() ) {
finish();
return;
}
mCurrentFolder = mPendingFolders.takeAt( 0 );
kDebug() << "===> Archiving next folder: " << mCurrentFolder.name();
const QString archivingStr( i18n( "Archiving folder %1", mCurrentFolder.name() ) );
if ( mProgressItem ) {
mProgressItem->setStatus( archivingStr );
}
KPIM::BroadcastStatus::instance()->setStatusMsg( archivingStr );
const QString folderName = mCurrentFolder.name();
bool success = true;
if ( hasChildren( mCurrentFolder ) ) {
if ( !writeDirHelper( subdirPathForCollection( mCurrentFolder ) ) ) {
success = false;
}
}
if (success) {
if ( !writeDirHelper( pathForCollection( mCurrentFolder ) ) ) {
success = false;
} else if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + QLatin1String("/cur") ) ) {
success = false;
} else if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + QLatin1String("/new") ) ) {
success = false;
} else if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + QLatin1String("/tmp") ) ) {
success = false;
}
}
if ( !success ) {
abort( i18n( "Unable to create folder structure for folder '%1' within archive file.",
mCurrentFolder.name() ) );
return;
}
Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob( mCurrentFolder );
job->setProperty( "folderName", folderName );
connect( job, SIGNAL(result(KJob*)), SLOT(onArchiveNextFolderDone(KJob*)) );
}
void BackupJob::onArchiveNextFolderDone( KJob *job )
{
if ( job->error() ) {
kWarning() << job->errorString();
abort( i18n( "Unable to get message list for folder %1.",
job->property( "folderName" ).toString() ) );
return;
}
Akonadi::ItemFetchJob *fetchJob = qobject_cast<Akonadi::ItemFetchJob*>( job );
mPendingMessages += fetchJob->items();
archiveNextMessage();
}
void BackupJob::start()
{
Q_ASSERT( !mMailArchivePath.isEmpty() );
Q_ASSERT( mRootFolder.isValid() );
if ( !queueFolders( mRootFolder ) ) {
return;
}
switch ( mArchiveType ) {
case Zip:
{
KZip *zip = new KZip( mMailArchivePath.path() );
zip->setCompression( KZip::DeflateCompression );
mArchive = zip;
break;
}
case Tar:
{
mArchive = new KTar( mMailArchivePath.path(), QLatin1String("application/x-tar") );
break;
}
case TarGz:
{
mArchive = new KTar( mMailArchivePath.path(), QLatin1String("application/x-gzip") );
break;
}
case TarBz2:
{
mArchive = new KTar( mMailArchivePath.path(), QLatin1String("application/x-bzip2") );
break;
}
}
kDebug() << "Starting backup.";
if ( !mArchive->open( QIODevice::WriteOnly ) ) {
abort( i18n( "Unable to open archive for writing." ) );
return;
}
mProgressItem = KPIM::ProgressManager::createProgressItem(
QLatin1String("BackupJob"),
i18n( "Archiving" ),
QString(),
true );
mProgressItem->setUsesBusyIndicator( true );
connect( mProgressItem, SIGNAL(progressItemCanceled(KPIM::ProgressItem*)),
this, SLOT(cancelJob()) );
archiveNextFolder();
}
void BackupJob::setDisplayMessageBox(bool display)
{
mDisplayMessageBox = display;
}