Akonadi

externalpartstorage.cpp
1/*
2 * SPDX-FileCopyrightText: 2015 Daniel Vrátil <dvratil@redhat.com>
3 *
4 * SPDX-License-Identifier: LGPL-2.1-or-later
5 *
6 */
7
8#include "akonadiprivate_debug.h"
9#include "externalpartstorage_p.h"
10#include "standarddirs_p.h"
11
12#include <QDir>
13#include <QFileInfo>
14#include <QMutexLocker>
15#include <QThread>
16
17using namespace Akonadi;
18
19ExternalPartStorageTransaction::ExternalPartStorageTransaction()
20{
21 ExternalPartStorage::self()->beginTransaction();
22}
23
24ExternalPartStorageTransaction::~ExternalPartStorageTransaction()
25{
26 if (ExternalPartStorage::self()->inTransaction()) {
27 rollback();
28 }
29}
30
31bool ExternalPartStorageTransaction::commit()
32{
33 return ExternalPartStorage::self()->commitTransaction();
34}
35
36bool ExternalPartStorageTransaction::rollback()
37{
38 return ExternalPartStorage::self()->rollbackTransaction();
39}
40
41ExternalPartStorage::ExternalPartStorage()
42{
43}
44
45ExternalPartStorage *ExternalPartStorage::self()
46{
47 static ExternalPartStorage sInstance;
48 return &sInstance;
49}
50
51QString ExternalPartStorage::resolveAbsolutePath(const QByteArray &filename, bool *exists, bool legacyFallback)
52{
53 return resolveAbsolutePath(QString::fromLocal8Bit(filename), exists, legacyFallback);
54}
55
56QString ExternalPartStorage::resolveAbsolutePath(const QString &filename, bool *exists, bool legacyFallback)
57{
58 if (exists) {
59 *exists = false;
60 }
61
62 QFileInfo finfo(filename);
63 if (finfo.isAbsolute()) {
64 if (exists && finfo.exists()) {
65 *exists = true;
66 }
67 return filename;
68 }
69
70 const QString basePath = StandardDirs::saveDir("data", QStringLiteral("file_db_data"));
71 Q_ASSERT(!basePath.isEmpty());
72
73 // Part files are stored in levelled cache. We use modulo 100 of the partID
74 // to ensure even distribution of the part files into the subfolders.
75 // PartID is encoded in filename as "PARTID_rX".
76 const int revPos = filename.indexOf(QLatin1Char('_'));
77 const QString path = basePath + QDir::separator() + (revPos > 1 ? filename[revPos - 2] : QLatin1Char('0'))
78 + (revPos > 0 ? filename[revPos - 1] : QLatin1Char('0')) + QDir::separator() + filename;
79 // If legacy fallback is disabled, return it in any case
80 if (!legacyFallback) {
81 QFileInfo finfo(path);
82 QDir().mkpath(finfo.path());
83 return path;
84 }
85
86 // ..otherwise return it only if it exists
87 if (QFile::exists(path)) {
88 if (exists) {
89 *exists = true;
90 }
91 return path;
92 }
93
94 // .. and fallback to legacy if it does not, but only when legacy exists
95 const QString legacyPath = basePath + QDir::separator() + filename;
96 if (QFile::exists(legacyPath)) {
97 if (exists) {
98 *exists = true;
99 }
100 return legacyPath;
101 } else {
102 QFileInfo legacyFinfo(path);
103 QDir().mkpath(legacyFinfo.path());
104 // If neither legacy or new path exists, return the new path, so that
105 // new items are created in the correct location
106 return path;
107 }
108}
109
110bool ExternalPartStorage::createPartFile(const QByteArray &data, qint64 partId, QByteArray &partFileName)
111{
112 bool exists = false;
113 partFileName = updateFileNameRevision(QByteArray::number(partId));
114 const QString path = resolveAbsolutePath(partFileName, &exists);
115 if (exists) {
116 qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to create a part" << partFileName << ", which already exists!";
117 return false;
118 }
119
120 QFile f(path);
121 if (!f.open(QIODevice::WriteOnly)) {
122 qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to open new part file for writing:" << f.errorString();
123 return false;
124 }
125 if (f.write(data) != data.size()) {
126 // TODO: Maybe just try again?
127 qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to write all data into the part file";
128 return false;
129 }
130 f.close();
131
132 if (inTransaction()) {
133 addToTransaction({{Operation::Create, path}});
134 }
135 return true;
136}
137
138bool ExternalPartStorage::updatePartFile(const QByteArray &newData, const QByteArray &partFile, QByteArray &newPartFile)
139{
140 bool exists = false;
141 const QString currentPartPath = resolveAbsolutePath(partFile, &exists);
142 if (!exists) {
143 qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to update a non-existent part, aborting update";
144 return false;
145 }
146
147 newPartFile = updateFileNameRevision(partFile);
148 exists = false;
149 const QString newPartPath = resolveAbsolutePath(newPartFile, &exists);
150 if (exists) {
151 qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to update part" << partFile << ", but" << newPartFile << "already exists, aborting update";
152 return false;
153 }
154
155 QFile f(newPartPath);
156 if (!f.open(QIODevice::WriteOnly)) {
157 qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to open new part file for update:" << f.errorString();
158 return false;
159 }
160
161 if (f.write(newData) != newData.size()) {
162 // TODO: Maybe just try again?
163 qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to write all data into the part file";
164 return false;
165 }
166 f.close();
167
168 if (inTransaction()) {
169 addToTransaction({{Operation::Create, newPartPath}, {Operation::Delete, currentPartPath}});
170 } else {
171 if (!QFile::remove(currentPartPath)) {
172 // Not a reason to fail the operation
173 qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to remove old part payload file" << currentPartPath;
174 }
175 }
176
177 return true;
178}
179
180bool ExternalPartStorage::removePartFile(const QString &partFile)
181{
182 if (inTransaction()) {
183 addToTransaction({{Operation::Delete, partFile}});
184 } else {
185 if (!QFile::remove(partFile)) {
186 // Not a reason to fail the operation
187 qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to remove part file" << partFile;
188 }
189 }
190
191 return true;
192}
193
194QByteArray ExternalPartStorage::updateFileNameRevision(const QByteArray &filename)
195{
196 const int revIndex = filename.indexOf("_r");
197 if (revIndex > -1) {
198 QByteArray rev = filename.mid(revIndex + 2);
199 int r = rev.toInt();
200 r++;
201 rev = QByteArray::number(r);
202 return filename.left(revIndex + 2) + rev;
203 }
204
205 return filename + "_r0";
206}
207
208QByteArray ExternalPartStorage::nameForPartId(qint64 partId)
209{
210 return QByteArray::number(partId) + "_r0";
211}
212
213bool ExternalPartStorage::beginTransaction()
214{
215 QMutexLocker locker(&mTransactionLock);
216 if (mTransactions.contains(QThread::currentThread())) {
217 qCWarning(AKONADIPRIVATE_LOG) << "Error: there is already a transaction in progress in this thread";
218 return false;
219 }
220
221 mTransactions.insert(QThread::currentThread(), QList<Operation>());
222 return true;
223}
224
225QString ExternalPartStorage::akonadiStoragePath()
226{
227 return StandardDirs::saveDir("data", QStringLiteral("file_db_data"));
228}
229
230bool ExternalPartStorage::commitTransaction()
231{
232 QMutexLocker locker(&mTransactionLock);
233 auto iter = mTransactions.find(QThread::currentThread());
234 if (iter == mTransactions.end()) {
235 qCWarning(AKONADIPRIVATE_LOG) << "Commit error: there is no transaction in progress in this thread";
236 return false;
237 }
238
239 const QList<Operation> trx = iter.value();
240 mTransactions.erase(iter);
241 locker.unlock();
242
243 return replayTransaction(trx, true);
244}
245
246bool ExternalPartStorage::rollbackTransaction()
247{
248 QMutexLocker locker(&mTransactionLock);
249 auto iter = mTransactions.find(QThread::currentThread());
250 if (iter == mTransactions.end()) {
251 qCWarning(AKONADIPRIVATE_LOG) << "Rollback error: there is no transaction in progress in this thread";
252 return false;
253 }
254
255 const QList<Operation> trx = iter.value();
256 mTransactions.erase(iter);
257 locker.unlock();
258
259 return replayTransaction(trx, false);
260}
261
262bool ExternalPartStorage::inTransaction() const
263{
264 QMutexLocker locker(&mTransactionLock);
265 return mTransactions.contains(QThread::currentThread());
266}
267
268void ExternalPartStorage::addToTransaction(const QList<Operation> &ops)
269{
270 QMutexLocker locker(&mTransactionLock);
271 auto iter = mTransactions.find(QThread::currentThread());
272 Q_ASSERT(iter != mTransactions.end());
273 locker.unlock();
274
275 for (const Operation &op : ops) {
276 iter->append(op);
277 }
278}
279
280bool ExternalPartStorage::replayTransaction(const QList<Operation> &trx, bool commit)
281{
282 for (auto iter = trx.constBegin(), end = trx.constEnd(); iter != end; ++iter) {
283 const Operation &op = *iter;
284
285 if (op.type == Operation::Create) {
286 if (commit) {
287 // no-op: we actually created that already in createPart()/updatePart()
288 } else {
289 if (!QFile::remove(op.filename)) {
290 // We failed to remove the file, but don't abort the rollback.
291 // This is an error, but does not cause data loss.
292 qCWarning(AKONADIPRIVATE_LOG) << "Warning: failed to remove" << op.filename << "while rolling back a transaction";
293 }
294 }
295 } else if (op.type == Operation::Delete) {
296 if (commit) {
297 if (!QFile::remove(op.filename)) {
298 // We failed to remove the file, but don't abort the commit.
299 // This is an error, but does not cause data loss.
300 qCWarning(AKONADIPRIVATE_LOG) << "Warning: failed to remove" << op.filename << "while committing a transaction";
301 }
302 } else {
303 // no-op: we did not actually delete the file yet
304 }
305 } else {
306 Q_UNREACHABLE();
307 }
308 }
309
310 return true;
311}
Helper integration between Akonadi and Qt.
QString path(const QString &relativePath)
const QList< QKeySequence > & end()
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
QByteArray left(qsizetype len) const const
QByteArray mid(qsizetype pos, qsizetype len) const const
QByteArray number(double n, char format, int precision)
qsizetype size() const const
int toInt(bool *ok, int base) const const
bool mkpath(const QString &dirPath) const const
QChar separator()
bool exists() const const
bool remove()
const_iterator constBegin() const const
const_iterator constEnd() const const
T value(qsizetype i) const const
QString fromLocal8Bit(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QThread * currentThread()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:13:38 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.