File indexing completed on 2024-09-15 04:36:24

0001 /*
0002  * SPDX-FileCopyrightText: 2015 Daniel Vrátil <dvratil@redhat.com>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.1-or-later
0005  *
0006  */
0007 
0008 #include "akonadiprivate_debug.h"
0009 #include "externalpartstorage_p.h"
0010 #include "standarddirs_p.h"
0011 
0012 #include <QDir>
0013 #include <QFileInfo>
0014 #include <QMutexLocker>
0015 #include <QThread>
0016 
0017 using namespace Akonadi;
0018 
0019 ExternalPartStorageTransaction::ExternalPartStorageTransaction()
0020 {
0021     ExternalPartStorage::self()->beginTransaction();
0022 }
0023 
0024 ExternalPartStorageTransaction::~ExternalPartStorageTransaction()
0025 {
0026     if (ExternalPartStorage::self()->inTransaction()) {
0027         rollback();
0028     }
0029 }
0030 
0031 bool ExternalPartStorageTransaction::commit()
0032 {
0033     return ExternalPartStorage::self()->commitTransaction();
0034 }
0035 
0036 bool ExternalPartStorageTransaction::rollback()
0037 {
0038     return ExternalPartStorage::self()->rollbackTransaction();
0039 }
0040 
0041 ExternalPartStorage::ExternalPartStorage()
0042 {
0043 }
0044 
0045 ExternalPartStorage *ExternalPartStorage::self()
0046 {
0047     static ExternalPartStorage sInstance;
0048     return &sInstance;
0049 }
0050 
0051 QString ExternalPartStorage::resolveAbsolutePath(const QByteArray &filename, bool *exists, bool legacyFallback)
0052 {
0053     return resolveAbsolutePath(QString::fromLocal8Bit(filename), exists, legacyFallback);
0054 }
0055 
0056 QString ExternalPartStorage::resolveAbsolutePath(const QString &filename, bool *exists, bool legacyFallback)
0057 {
0058     if (exists) {
0059         *exists = false;
0060     }
0061 
0062     QFileInfo finfo(filename);
0063     if (finfo.isAbsolute()) {
0064         if (exists && finfo.exists()) {
0065             *exists = true;
0066         }
0067         return filename;
0068     }
0069 
0070     const QString basePath = StandardDirs::saveDir("data", QStringLiteral("file_db_data"));
0071     Q_ASSERT(!basePath.isEmpty());
0072 
0073     // Part files are stored in levelled cache. We use modulo 100 of the partID
0074     // to ensure even distribution of the part files into the subfolders.
0075     // PartID is encoded in filename as "PARTID_rX".
0076     const int revPos = filename.indexOf(QLatin1Char('_'));
0077     const QString path = basePath + QDir::separator() + (revPos > 1 ? filename[revPos - 2] : QLatin1Char('0'))
0078         + (revPos > 0 ? filename[revPos - 1] : QLatin1Char('0')) + QDir::separator() + filename;
0079     // If legacy fallback is disabled, return it in any case
0080     if (!legacyFallback) {
0081         QFileInfo finfo(path);
0082         QDir().mkpath(finfo.path());
0083         return path;
0084     }
0085 
0086     // ..otherwise return it only if it exists
0087     if (QFile::exists(path)) {
0088         if (exists) {
0089             *exists = true;
0090         }
0091         return path;
0092     }
0093 
0094     // .. and fallback to legacy if it does not, but only when legacy exists
0095     const QString legacyPath = basePath + QDir::separator() + filename;
0096     if (QFile::exists(legacyPath)) {
0097         if (exists) {
0098             *exists = true;
0099         }
0100         return legacyPath;
0101     } else {
0102         QFileInfo legacyFinfo(path);
0103         QDir().mkpath(legacyFinfo.path());
0104         // If neither legacy or new path exists, return the new path, so that
0105         // new items are created in the correct location
0106         return path;
0107     }
0108 }
0109 
0110 bool ExternalPartStorage::createPartFile(const QByteArray &data, qint64 partId, QByteArray &partFileName)
0111 {
0112     bool exists = false;
0113     partFileName = updateFileNameRevision(QByteArray::number(partId));
0114     const QString path = resolveAbsolutePath(partFileName, &exists);
0115     if (exists) {
0116         qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to create a part" << partFileName << ", which already exists!";
0117         return false;
0118     }
0119 
0120     QFile f(path);
0121     if (!f.open(QIODevice::WriteOnly)) {
0122         qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to open new part file for writing:" << f.errorString();
0123         return false;
0124     }
0125     if (f.write(data) != data.size()) {
0126         // TODO: Maybe just try again?
0127         qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to write all data into the part file";
0128         return false;
0129     }
0130     f.close();
0131 
0132     if (inTransaction()) {
0133         addToTransaction({{Operation::Create, path}});
0134     }
0135     return true;
0136 }
0137 
0138 bool ExternalPartStorage::updatePartFile(const QByteArray &newData, const QByteArray &partFile, QByteArray &newPartFile)
0139 {
0140     bool exists = false;
0141     const QString currentPartPath = resolveAbsolutePath(partFile, &exists);
0142     if (!exists) {
0143         qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to update a non-existent part, aborting update";
0144         return false;
0145     }
0146 
0147     newPartFile = updateFileNameRevision(partFile);
0148     exists = false;
0149     const QString newPartPath = resolveAbsolutePath(newPartFile, &exists);
0150     if (exists) {
0151         qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to update part" << partFile << ", but" << newPartFile << "already exists, aborting update";
0152         return false;
0153     }
0154 
0155     QFile f(newPartPath);
0156     if (!f.open(QIODevice::WriteOnly)) {
0157         qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to open new part file for update:" << f.errorString();
0158         return false;
0159     }
0160 
0161     if (f.write(newData) != newData.size()) {
0162         // TODO: Maybe just try again?
0163         qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to write all data into the part file";
0164         return false;
0165     }
0166     f.close();
0167 
0168     if (inTransaction()) {
0169         addToTransaction({{Operation::Create, newPartPath}, {Operation::Delete, currentPartPath}});
0170     } else {
0171         if (!QFile::remove(currentPartPath)) {
0172             // Not a reason to fail the operation
0173             qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to remove old part payload file" << currentPartPath;
0174         }
0175     }
0176 
0177     return true;
0178 }
0179 
0180 bool ExternalPartStorage::removePartFile(const QString &partFile)
0181 {
0182     if (inTransaction()) {
0183         addToTransaction({{Operation::Delete, partFile}});
0184     } else {
0185         if (!QFile::remove(partFile)) {
0186             // Not a reason to fail the operation
0187             qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to remove part file" << partFile;
0188         }
0189     }
0190 
0191     return true;
0192 }
0193 
0194 QByteArray ExternalPartStorage::updateFileNameRevision(const QByteArray &filename)
0195 {
0196     const int revIndex = filename.indexOf("_r");
0197     if (revIndex > -1) {
0198         QByteArray rev = filename.mid(revIndex + 2);
0199         int r = rev.toInt();
0200         r++;
0201         rev = QByteArray::number(r);
0202         return filename.left(revIndex + 2) + rev;
0203     }
0204 
0205     return filename + "_r0";
0206 }
0207 
0208 QByteArray ExternalPartStorage::nameForPartId(qint64 partId)
0209 {
0210     return QByteArray::number(partId) + "_r0";
0211 }
0212 
0213 bool ExternalPartStorage::beginTransaction()
0214 {
0215     QMutexLocker locker(&mTransactionLock);
0216     if (mTransactions.contains(QThread::currentThread())) {
0217         qCWarning(AKONADIPRIVATE_LOG) << "Error: there is already a transaction in progress in this thread";
0218         return false;
0219     }
0220 
0221     mTransactions.insert(QThread::currentThread(), QList<Operation>());
0222     return true;
0223 }
0224 
0225 QString ExternalPartStorage::akonadiStoragePath()
0226 {
0227     return StandardDirs::saveDir("data", QStringLiteral("file_db_data"));
0228 }
0229 
0230 bool ExternalPartStorage::commitTransaction()
0231 {
0232     QMutexLocker locker(&mTransactionLock);
0233     auto iter = mTransactions.find(QThread::currentThread());
0234     if (iter == mTransactions.end()) {
0235         qCWarning(AKONADIPRIVATE_LOG) << "Commit error: there is no transaction in progress in this thread";
0236         return false;
0237     }
0238 
0239     const QList<Operation> trx = iter.value();
0240     mTransactions.erase(iter);
0241     locker.unlock();
0242 
0243     return replayTransaction(trx, true);
0244 }
0245 
0246 bool ExternalPartStorage::rollbackTransaction()
0247 {
0248     QMutexLocker locker(&mTransactionLock);
0249     auto iter = mTransactions.find(QThread::currentThread());
0250     if (iter == mTransactions.end()) {
0251         qCWarning(AKONADIPRIVATE_LOG) << "Rollback error: there is no transaction in progress in this thread";
0252         return false;
0253     }
0254 
0255     const QList<Operation> trx = iter.value();
0256     mTransactions.erase(iter);
0257     locker.unlock();
0258 
0259     return replayTransaction(trx, false);
0260 }
0261 
0262 bool ExternalPartStorage::inTransaction() const
0263 {
0264     QMutexLocker locker(&mTransactionLock);
0265     return mTransactions.contains(QThread::currentThread());
0266 }
0267 
0268 void ExternalPartStorage::addToTransaction(const QList<Operation> &ops)
0269 {
0270     QMutexLocker locker(&mTransactionLock);
0271     auto iter = mTransactions.find(QThread::currentThread());
0272     Q_ASSERT(iter != mTransactions.end());
0273     locker.unlock();
0274 
0275     for (const Operation &op : ops) {
0276         iter->append(op);
0277     }
0278 }
0279 
0280 bool ExternalPartStorage::replayTransaction(const QList<Operation> &trx, bool commit)
0281 {
0282     for (auto iter = trx.constBegin(), end = trx.constEnd(); iter != end; ++iter) {
0283         const Operation &op = *iter;
0284 
0285         if (op.type == Operation::Create) {
0286             if (commit) {
0287                 // no-op: we actually created that already in createPart()/updatePart()
0288             } else {
0289                 if (!QFile::remove(op.filename)) {
0290                     // We failed to remove the file, but don't abort the rollback.
0291                     // This is an error, but does not cause data loss.
0292                     qCWarning(AKONADIPRIVATE_LOG) << "Warning: failed to remove" << op.filename << "while rolling back a transaction";
0293                 }
0294             }
0295         } else if (op.type == Operation::Delete) {
0296             if (commit) {
0297                 if (!QFile::remove(op.filename)) {
0298                     // We failed to remove the file, but don't abort the commit.
0299                     // This is an error, but does not cause data loss.
0300                     qCWarning(AKONADIPRIVATE_LOG) << "Warning: failed to remove" << op.filename << "while committing a transaction";
0301                 }
0302             } else {
0303                 // no-op: we did not actually delete the file yet
0304             }
0305         } else {
0306             Q_UNREACHABLE();
0307         }
0308     }
0309 
0310     return true;
0311 }