Akonadi

externalpartstorage.cpp
1 /*
2  * SPDX-FileCopyrightText: 2015 Daniel Vrátil <[email protected]>
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 
19 ExternalPartStorageTransaction::ExternalPartStorageTransaction()
20 {
21  ExternalPartStorage::self()->beginTransaction();
22 }
23 
24 ExternalPartStorageTransaction::~ExternalPartStorageTransaction()
25 {
26  if (ExternalPartStorage::self()->inTransaction()) {
27  rollback();
28  }
29 }
30 
31 bool ExternalPartStorageTransaction::commit()
32 {
33  return ExternalPartStorage::self()->commitTransaction();
34 }
35 
36 bool ExternalPartStorageTransaction::rollback()
37 {
38  return ExternalPartStorage::self()->rollbackTransaction();
39 }
40 
41 ExternalPartStorage::ExternalPartStorage()
42 {
43 }
44 
45 ExternalPartStorage *ExternalPartStorage::self()
46 {
47  static ExternalPartStorage sInstance;
48  return &sInstance;
49 }
50 
51 QString ExternalPartStorage::resolveAbsolutePath(const QByteArray &filename, bool *exists, bool legacyFallback)
52 {
53  return resolveAbsolutePath(QString::fromLocal8Bit(filename), exists, legacyFallback);
54 }
55 
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 
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 
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 
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 
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 
208 QByteArray ExternalPartStorage::nameForPartId(qint64 partId)
209 {
210  return QByteArray::number(partId) + "_r0";
211 }
212 
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 
225 QString ExternalPartStorage::akonadiStoragePath()
226 {
227  return StandardDirs::saveDir("data", QStringLiteral("file_db_data"));
228 }
229 
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 
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 
262 bool ExternalPartStorage::inTransaction() const
263 {
264  QMutexLocker locker(&mTransactionLock);
265  return mTransactions.contains(QThread::currentThread());
266 }
267 
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 
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 }
bool remove()
int indexOf(char ch, int from) const const
QChar separator()
QByteArray number(int n, int base)
QVector::const_iterator constEnd() const const
bool exists() const const
T value(int i) const const
QString fromLocal8Bit(const char *str, int size)
QByteArray mid(int pos, int len) const const
bool isEmpty() const const
bool mkpath(const QString &dirPath) const const
QThread * currentThread()
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QByteArray left(int len) const const
QString path(const QString &relativePath)
int size() const const
QVector::const_iterator constBegin() const const
Helper integration between Akonadi and Qt.
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon May 8 2023 03:52:16 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.