File indexing completed on 2024-11-10 04:40:47
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 }