Akonadi

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

KDE's Doxygen guidelines are available online.