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.
503 lines
16 KiB
503 lines
16 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 "kmmsgdict.h" |
|
#include "kmfolder.h" |
|
#include "kmfoldercachedimap.h" |
|
#include "kmfolderdir.h" |
|
#include "folderutil.h" |
|
|
|
#include "progressmanager.h" |
|
|
|
#include "kzip.h" |
|
#include "ktar.h" |
|
#include "kmessagebox.h" |
|
|
|
#include "qfile.h" |
|
#include "qfileinfo.h" |
|
#include "qstringlist.h" |
|
|
|
using namespace KMail; |
|
|
|
BackupJob::BackupJob( QWidget *parent ) |
|
: QObject( parent ), |
|
mArchiveType( Zip ), |
|
mRootFolder( 0 ), |
|
mArchive( 0 ), |
|
mParentWidget( parent ), |
|
mCurrentFolderOpen( false ), |
|
mArchivedMessages( 0 ), |
|
mArchivedSize( 0 ), |
|
mProgressItem( 0 ), |
|
mAborted( false ), |
|
mDeleteFoldersAfterCompletion( false ), |
|
mCurrentFolder( 0 ), |
|
mCurrentMessage( 0 ), |
|
mCurrentJob( 0 ) |
|
{ |
|
} |
|
|
|
BackupJob::~BackupJob() |
|
{ |
|
mPendingFolders.clear(); |
|
if ( mArchive ) { |
|
delete mArchive; |
|
mArchive = 0; |
|
} |
|
} |
|
|
|
void BackupJob::setRootFolder( KMFolder *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; |
|
} |
|
|
|
QString BackupJob::stripRootPath( const QString &path ) const |
|
{ |
|
QString ret = path; |
|
ret = ret.remove( mRootFolder->path() ); |
|
if ( ret.startsWith( QLatin1String( "/" ) ) ) |
|
ret = ret.right( ret.length() - 1 ); |
|
return ret; |
|
} |
|
|
|
void BackupJob::queueFolders( KMFolder *root ) |
|
{ |
|
mPendingFolders.append( root ); |
|
kDebug() << "Queueing folder " << root->name(); |
|
KMFolderDir *dir = root->child(); |
|
if ( dir ) { |
|
QListIterator<KMFolderNode*> it( *dir ); |
|
while ( it.hasNext() ) { |
|
KMFolderNode *node = it.next(); |
|
if ( node->isDir() ) |
|
continue; |
|
KMFolder *folder = static_cast<KMFolder*>( node ); |
|
queueFolders( folder ); |
|
} |
|
} |
|
} |
|
|
|
bool BackupJob::hasChildren( KMFolder *folder ) const |
|
{ |
|
KMFolderDir *dir = folder->child(); |
|
if ( dir ) { |
|
QListIterator<KMFolderNode*> it( *dir ); |
|
while ( it.hasNext() ) { |
|
KMFolderNode *node = it.next(); |
|
if ( !node->isDir() ) |
|
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 ( mCurrentFolderOpen && mCurrentFolder ) { |
|
mCurrentFolder->close( "BackupJob" ); |
|
mCurrentFolder = 0; |
|
} |
|
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! |
|
FolderUtil::deleteFolder( mRootFolder, mParentWidget ); |
|
} |
|
} |
|
|
|
deleteLater(); |
|
} |
|
|
|
void BackupJob::archiveNextMessage() |
|
{ |
|
if ( mAborted ) |
|
return; |
|
|
|
mCurrentMessage = 0; |
|
if ( mPendingMessages.isEmpty() ) { |
|
kDebug() << "===> All messages done in folder " << mCurrentFolder->name(); |
|
mCurrentFolder->close( "BackupJob" ); |
|
mCurrentFolderOpen = false; |
|
archiveNextFolder(); |
|
return; |
|
} |
|
|
|
unsigned long serNum = mPendingMessages.front(); |
|
mPendingMessages.pop_front(); |
|
|
|
KMFolder *folder; |
|
mMessageIndex = -1; |
|
KMMsgDict::instance()->getLocation( serNum, &folder, &mMessageIndex ); |
|
if ( mMessageIndex == -1 ) { |
|
kWarning() << "Failed to get message location for sernum " << serNum; |
|
abort( i18n( "Unable to retrieve a message for folder '%1'.", mCurrentFolder->name() ) ); |
|
return; |
|
} |
|
|
|
Q_ASSERT( folder == mCurrentFolder ); |
|
const KMMsgBase *base = mCurrentFolder->getMsgBase( mMessageIndex ); |
|
mUnget = base && !base->isMessage(); |
|
KMMessage *message = mCurrentFolder->getMsg( mMessageIndex ); |
|
if ( !message ) { |
|
kWarning() << "Failed to retrieve message with index " << mMessageIndex; |
|
abort( i18n( "Unable to retrieve a message for folder '%1'.", mCurrentFolder->name() ) ); |
|
return; |
|
} |
|
|
|
kDebug() << "Going to get next message with subject " << message->subject() << ", " |
|
<< mPendingMessages.size() << " messages left in the folder."; |
|
|
|
if ( message->isComplete() ) { |
|
// Use a singleshot timer, or otherwise we risk ending up in a very big recursion |
|
// for folders that have many messages |
|
mCurrentMessage = message; |
|
QTimer::singleShot( 0, this, SLOT( processCurrentMessage() ) ); |
|
} |
|
else if ( message->parent() ) { |
|
mCurrentJob = message->parent()->createJob( message ); |
|
mCurrentJob->setCancellable( false ); |
|
connect( mCurrentJob, SIGNAL( messageRetrieved( KMMessage* ) ), |
|
this, SLOT( messageRetrieved( KMMessage* ) ) ); |
|
connect( mCurrentJob, SIGNAL( result( KMail::FolderJob* ) ), |
|
this, SLOT( folderJobFinished( KMail::FolderJob* ) ) ); |
|
mCurrentJob->start(); |
|
} |
|
else { |
|
kWarning() << "Message with subject " << mCurrentMessage->subject() |
|
<< " is neither complete nor has a parent!"; |
|
abort( i18n( "Internal error while trying to retrieve a message from folder '%1'.", |
|
mCurrentFolder->name() ) ); |
|
} |
|
} |
|
|
|
static int fileInfoToUnixPermissions( const QFileInfo &fileInfo ) |
|
{ |
|
int perm = 0; |
|
if ( fileInfo.permission( QFile::ExeOther ) ) perm += S_IXOTH; |
|
if ( fileInfo.permission( QFile::WriteOther ) ) perm += S_IWOTH; |
|
if ( fileInfo.permission( QFile::ReadOther ) ) perm += S_IROTH; |
|
if ( fileInfo.permission( QFile::ExeGroup ) ) perm += S_IXGRP; |
|
if ( fileInfo.permission( QFile::WriteGroup ) ) perm += S_IWGRP; |
|
if ( fileInfo.permission( QFile::ReadGroup ) ) perm += S_IRGRP; |
|
if ( fileInfo.permission( QFile::ExeOwner ) ) perm += S_IXUSR; |
|
if ( fileInfo.permission( QFile::WriteOwner ) ) perm += S_IWUSR; |
|
if ( fileInfo.permission( QFile::ReadOwner ) ) perm += S_IRUSR; |
|
return perm; |
|
} |
|
|
|
void BackupJob::processCurrentMessage() |
|
{ |
|
if ( mAborted ) |
|
return; |
|
|
|
if ( mCurrentMessage ) { |
|
kDebug() << "Processing message with subject " << mCurrentMessage->subject(); |
|
const DwString &messageDWString = mCurrentMessage->asDwString(); |
|
const qint64 messageSize = messageDWString.size(); |
|
const char *messageString = mCurrentMessage->asDwString().c_str(); |
|
QString messageName; |
|
QFileInfo fileInfo; |
|
if ( messageName.isEmpty() ) { |
|
messageName = QString::number( mCurrentMessage->getMsgSerNum() ); // IMAP doesn't have filenames |
|
if ( mCurrentMessage->storage() ) { |
|
fileInfo.setFile( mCurrentMessage->storage()->location() ); |
|
// TODO: what permissions etc to take when there is no storage file? |
|
} |
|
} |
|
else { |
|
// TODO: What if the message is not in the "cur" directory? |
|
fileInfo.setFile( mCurrentFolder->location() + "/cur/" + mCurrentMessage->fileName() ); |
|
messageName = mCurrentMessage->fileName(); |
|
} |
|
|
|
const QString fileName = stripRootPath( mCurrentFolder->location() ) + |
|
"/cur/" + messageName; |
|
|
|
QString user; |
|
QString group; |
|
mode_t permissions = 0700; |
|
time_t creationTime = time( 0 ); |
|
time_t modificationTime = time( 0 ); |
|
time_t accessTime = time( 0 ); |
|
if ( !fileInfo.fileName().isEmpty() ) { |
|
user = fileInfo.owner(); |
|
group = fileInfo.group(); |
|
permissions = fileInfoToUnixPermissions( fileInfo ); |
|
creationTime = fileInfo.created().toTime_t(); |
|
modificationTime = fileInfo.lastModified().toTime_t(); |
|
accessTime = fileInfo.lastRead().toTime_t(); |
|
} |
|
else { |
|
kWarning() << "Unable to find file for message " << fileName; |
|
} |
|
|
|
if ( !mArchive->writeFile( fileName, user, group, |
|
messageString, messageSize, permissions, |
|
accessTime, modificationTime, creationTime ) ) { |
|
abort( i18n( "Failed to write a message into the archive folder '%1'.", mCurrentFolder->name() ) ); |
|
return; |
|
} |
|
|
|
if ( mUnget ) { |
|
Q_ASSERT( mMessageIndex >= 0 ); |
|
mCurrentFolder->unGetMsg( mMessageIndex ); |
|
} |
|
|
|
mArchivedMessages++; |
|
mArchivedSize += messageSize; |
|
} |
|
else { |
|
// No message? According to ImapJob::slotGetMessageResult(), that means the message is no |
|
// longer on the server. So ignore this one. |
|
kWarning() << "Unable to download a message for folder " << mCurrentFolder->name(); |
|
} |
|
archiveNextMessage(); |
|
} |
|
|
|
void BackupJob::messageRetrieved( KMMessage *message ) |
|
{ |
|
mCurrentMessage = message; |
|
processCurrentMessage(); |
|
} |
|
|
|
void BackupJob::folderJobFinished( KMail::FolderJob *job ) |
|
{ |
|
if ( mAborted ) |
|
return; |
|
|
|
// The job might finish after it has emitted messageRetrieved(), in which case we have already |
|
// started a new job. Don't set the current job to 0 in that case. |
|
if ( job == mCurrentJob ) { |
|
mCurrentJob = 0; |
|
} |
|
|
|
if ( job->error() ) { |
|
if ( mCurrentFolder ) |
|
abort( i18n( "Downloading a message in folder '%1' failed.", mCurrentFolder->name() ) ); |
|
else |
|
abort( i18n( "Downloading a message in the current folder failed." ) ); |
|
} |
|
} |
|
|
|
bool BackupJob::writeDirHelper( const QString &directoryPath, const QString &permissionPath ) |
|
{ |
|
QFileInfo fileInfo( permissionPath ); |
|
QString user = fileInfo.owner(); |
|
QString group = fileInfo.group(); |
|
mode_t permissions = fileInfoToUnixPermissions( fileInfo ); |
|
time_t creationTime = fileInfo.created().toTime_t(); |
|
time_t modificationTime = fileInfo.lastModified().toTime_t(); |
|
time_t accessTime = fileInfo.lastRead().toTime_t(); |
|
return mArchive->writeDir( stripRootPath( directoryPath ), user, group, permissions, accessTime, |
|
modificationTime, creationTime ); |
|
} |
|
|
|
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() ) ); |
|
if ( mCurrentFolder->open( "BackupJob" ) != 0 ) { |
|
abort( i18n( "Unable to open folder '%1'.", mCurrentFolder->name() ) ); |
|
return; |
|
} |
|
mCurrentFolderOpen = true; |
|
|
|
const QString folderName = mCurrentFolder->name(); |
|
bool success = true; |
|
if ( hasChildren( mCurrentFolder ) ) { |
|
if ( !writeDirHelper( mCurrentFolder->subdirLocation(), mCurrentFolder->subdirLocation() ) ) |
|
success = false; |
|
} |
|
if ( !writeDirHelper( mCurrentFolder->location(), mCurrentFolder->location() ) ) |
|
success = false; |
|
if ( !writeDirHelper( mCurrentFolder->location() + "/cur", mCurrentFolder->location() ) ) |
|
success = false; |
|
if ( !writeDirHelper( mCurrentFolder->location() + "/new", mCurrentFolder->location() ) ) |
|
success = false; |
|
if ( !writeDirHelper( mCurrentFolder->location() + "/tmp", mCurrentFolder->location() ) ) |
|
success = false; |
|
if ( !success ) { |
|
abort( i18n( "Unable to create folder structure for folder '%1' within archive file.", |
|
mCurrentFolder->name() ) ); |
|
return; |
|
} |
|
|
|
for ( int i = 0; i < mCurrentFolder->count( false /* no cache */ ); i++ ) { |
|
unsigned long serNum = KMMsgDict::instance()->getMsgSerNum( mCurrentFolder, i ); |
|
if ( serNum == 0 ) { |
|
// Uh oh |
|
kWarning() << "Got serial number zero in " << mCurrentFolder->name() |
|
<< " at index " << i << "!"; |
|
// TODO: handle error in a nicer way. this is _very_ bad |
|
abort( i18n( "Unable to backup messages in folder '%1', the index file is corrupted.", |
|
mCurrentFolder->name() ) ); |
|
return; |
|
} |
|
else |
|
mPendingMessages.append( serNum ); |
|
} |
|
archiveNextMessage(); |
|
} |
|
|
|
// TODO |
|
// - error handling |
|
// - import |
|
// - connect to progressmanager, especially abort |
|
// - messagebox when finished (?) |
|
// - ui dialog |
|
// - use correct permissions |
|
// - save index and serial number? |
|
// - guarded pointers for folders |
|
// - online IMAP: check mails first, so sernums are up-to-date? |
|
// - "ignore errors"-mode, with summary how many messages couldn't be archived? |
|
// - do something when the user quits KMail while the backup job is running |
|
// - run in a thread? |
|
// - delete source folder after completion. dangerous!!! |
|
// |
|
// BUGS |
|
// - Online IMAP: Test Mails -> Test%20Mails |
|
// - corrupted sernums indices stop backup job |
|
void BackupJob::start() |
|
{ |
|
Q_ASSERT( !mMailArchivePath.isEmpty() ); |
|
Q_ASSERT( mRootFolder ); |
|
|
|
queueFolders( mRootFolder ); |
|
|
|
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" |
|
|
|
|