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