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
17 using namespace Akonadi;
18
ExternalPartStorageTransaction()19 ExternalPartStorageTransaction::ExternalPartStorageTransaction()
20 {
21 ExternalPartStorage::self()->beginTransaction();
22 }
23
~ExternalPartStorageTransaction()24 ExternalPartStorageTransaction::~ExternalPartStorageTransaction()
25 {
26 if (ExternalPartStorage::self()->inTransaction()) {
27 rollback();
28 }
29 }
30
commit()31 bool ExternalPartStorageTransaction::commit()
32 {
33 return ExternalPartStorage::self()->commitTransaction();
34 }
35
rollback()36 bool ExternalPartStorageTransaction::rollback()
37 {
38 return ExternalPartStorage::self()->rollbackTransaction();
39 }
40
ExternalPartStorage()41 ExternalPartStorage::ExternalPartStorage()
42 {
43 }
44
self()45 ExternalPartStorage *ExternalPartStorage::self()
46 {
47 static ExternalPartStorage sInstance;
48 return &sInstance;
49 }
50
resolveAbsolutePath(const QByteArray & filename,bool * exists,bool legacyFallback)51 QString ExternalPartStorage::resolveAbsolutePath(const QByteArray &filename, bool *exists, bool legacyFallback)
52 {
53 return resolveAbsolutePath(QString::fromLocal8Bit(filename), exists, legacyFallback);
54 }
55
resolveAbsolutePath(const QString & filename,bool * exists,bool legacyFallback)56 QString 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
createPartFile(const QByteArray & data,qint64 partId,QByteArray & partFileName)110 bool 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
updatePartFile(const QByteArray & newData,const QByteArray & partFile,QByteArray & newPartFile)138 bool 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
removePartFile(const QString & partFile)180 bool 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
updateFileNameRevision(const QByteArray & filename)194 QByteArray 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
nameForPartId(qint64 partId)208 QByteArray ExternalPartStorage::nameForPartId(qint64 partId)
209 {
210 return QByteArray::number(partId) + "_r0";
211 }
212
beginTransaction()213 bool 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(), QVector<Operation>());
222 return true;
223 }
224
akonadiStoragePath()225 QString ExternalPartStorage::akonadiStoragePath()
226 {
227 return StandardDirs::saveDir("data", QStringLiteral("file_db_data"));
228 }
229
commitTransaction()230 bool 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 QVector<Operation> trx = iter.value();
240 mTransactions.erase(iter);
241 locker.unlock();
242
243 return replayTransaction(trx, true);
244 }
245
rollbackTransaction()246 bool 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 QVector<Operation> trx = iter.value();
256 mTransactions.erase(iter);
257 locker.unlock();
258
259 return replayTransaction(trx, false);
260 }
261
inTransaction() const262 bool ExternalPartStorage::inTransaction() const
263 {
264 QMutexLocker locker(&mTransactionLock);
265 return mTransactions.contains(QThread::currentThread());
266 }
267
addToTransaction(const QVector<Operation> & ops)268 void ExternalPartStorage::addToTransaction(const QVector<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
replayTransaction(const QVector<Operation> & trx,bool commit)280 bool ExternalPartStorage::replayTransaction(const QVector<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 }
312