You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
405 lines
12 KiB
405 lines
12 KiB
/* Copyright 2009 Klarälvdalens Datakonsult AB |
|
|
|
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 "progressmanager.h" |
|
|
|
#include <Akonadi/CollectionDeleteJob> |
|
#include <Akonadi/CollectionFetchJob> |
|
#include <Akonadi/CollectionFetchScope> |
|
#include <Akonadi/ItemFetchJob> |
|
#include <Akonadi/ItemFetchScope> |
|
#include <KMime/Message> |
|
|
|
#include <klocale.h> |
|
#include <kzip.h> |
|
#include <ktar.h> |
|
#include <kmessagebox.h> |
|
#include <kio/global.h> |
|
|
|
#include <QFileInfo> |
|
#include <QTimer> |
|
|
|
using namespace KMail; |
|
|
|
BackupJob::BackupJob( QWidget *parent ) |
|
: QObject( parent ), |
|
mArchiveType( Zip ), |
|
mRootFolder( 0 ), |
|
mArchive( 0 ), |
|
mParentWidget( parent ), |
|
mArchivedMessages( 0 ), |
|
mArchivedSize( 0 ), |
|
mProgressItem( 0 ), |
|
mAborted( false ), |
|
mDeleteFoldersAfterCompletion( false ), |
|
mCurrentFolder( Akonadi::Collection() ), |
|
mCurrentJob( 0 ) |
|
{ |
|
} |
|
|
|
BackupJob::~BackupJob() |
|
{ |
|
mPendingFolders.clear(); |
|
delete mArchive; |
|
mArchive = 0; |
|
} |
|
|
|
void BackupJob::setRootFolder( const Akonadi::Collection &rootFolder ) |
|
{ |
|
mRootFolder = rootFolder; |
|
} |
|
|
|
void BackupJob::setSaveLocation( const KUrl& savePath ) |
|
{ |
|
mMailArchivePath = savePath; |
|
} |
|
|
|
void BackupJob::setArchiveType( ArchiveType type ) |
|
{ |
|
mArchiveType = type; |
|
} |
|
|
|
void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem ) |
|
{ |
|
mDeleteFoldersAfterCompletion = deleteThem; |
|
} |
|
|
|
bool BackupJob::queueFolders( const Akonadi::Collection &root ) |
|
{ |
|
mPendingFolders.append( root ); |
|
// 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 += '\n' + errorMessage; |
|
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; |
|
} |
|
} |
|
|
|
mProgressItem->setStatus( i18n( "Archiving finished" ) ); |
|
mProgressItem->setComplete(); |
|
mProgressItem = 0; |
|
|
|
QFileInfo archiveFileInfo( mMailArchivePath.path() ); |
|
QString text = i18n( "Archiving folder '%1' successfully completed. " |
|
"The archive was written to the file '%2'.", |
|
mRootFolder.name(), mMailArchivePath.path() ); |
|
text += '\n' + i18np( "1 message of size %2 was archived.", |
|
"%1 messages with the total size of %2 were archived.", |
|
mArchivedMessages, KIO::convertSize( mArchivedSize ) ); |
|
text += '\n' + i18n( "The archive file has a size of %1.", |
|
KIO::convertSize( archiveFileInfo.size() ) ); |
|
KMessageBox::information( mParentWidget, text, i18n( "Archiving finished." ) ); |
|
|
|
if ( mDeleteFoldersAfterCompletion ) { |
|
// Some safety checks first... |
|
if ( archiveFileInfo.size() > 0 && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) { |
|
// Sorry for any data loss! |
|
new Akonadi::CollectionDeleteJob( mRootFolder ); |
|
} |
|
} |
|
|
|
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 ) + "/cur/" + messageName; |
|
|
|
// PORT ME: user and group! |
|
kDebug() << "AKONDI PORT: disabled code here!"; |
|
if ( !mArchive->writeFile( fileName, "user", "group", messageData, messageSize ) ) { |
|
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, "user", "group" ); |
|
} |
|
|
|
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( '.' + collectionName( curCol ) + ".directory" + '/' ); |
|
curCol = curCol.parentCollection(); |
|
} |
|
Q_ASSERT( curCol == mRootFolder ); |
|
fullPath.prepend( '.' + collectionName( curCol ) + ".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( '.' + collection.name() + ".directory" ); |
|
return path; |
|
} |
|
|
|
void BackupJob::archiveNextFolder() |
|
{ |
|
if ( mAborted ) |
|
return; |
|
|
|
if ( mPendingFolders.isEmpty() ) { |
|
finish(); |
|
return; |
|
} |
|
|
|
mCurrentFolder = mPendingFolders.takeAt( 0 ); |
|
kDebug() << "===> Archiving next folder: " << mCurrentFolder.name(); |
|
mProgressItem->setStatus( i18n( "Archiving folder %1", mCurrentFolder.name() ) ); |
|
|
|
const QString folderName = mCurrentFolder.name(); |
|
bool success = true; |
|
if ( hasChildren( mCurrentFolder ) ) { |
|
if ( !writeDirHelper( subdirPathForCollection( mCurrentFolder ) ) ) |
|
success = false; |
|
} |
|
if ( !writeDirHelper( pathForCollection( mCurrentFolder ) ) ) |
|
success = false; |
|
if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + "/cur" ) ) |
|
success = false; |
|
if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + "/new" ) ) |
|
success = false; |
|
if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + "/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(), "application/x-tar" ); |
|
break; |
|
} |
|
case TarGz: { |
|
mArchive = new KTar( mMailArchivePath.path(), "application/x-gzip" ); |
|
break; |
|
} |
|
case TarBz2: { |
|
mArchive = new KTar( mMailArchivePath.path(), "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( |
|
"BackupJob", |
|
i18n( "Archiving" ), |
|
QString(), |
|
true ); |
|
mProgressItem->setUsesBusyIndicator( true ); |
|
connect( mProgressItem, SIGNAL(progressItemCanceled(KPIM::ProgressItem*)), |
|
this, SLOT(cancelJob()) ); |
|
|
|
archiveNextFolder(); |
|
} |
|
|
|
#include "backupjob.moc" |
|
|
|
|