1 /***************************************************************************
2  *   Copyright (C) 2005-2009 by Rajko Albrecht  ral@alwins-world.de        *
3  *   http://kdesvn.alwins-world.de/                                        *
4  *                                                                         *
5  * This program is free software; you can redistribute it and/or           *
6  * modify it under the terms of the GNU Lesser General Public              *
7  * License as published by the Free Software Foundation; either            *
8  * version 2.1 of the License, or (at your option) any later version.      *
9  *                                                                         *
10  * This program is distributed in the hope that it will be useful,         *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of          *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU       *
13  * Lesser General Public License for more details.                         *
14  *                                                                         *
15  * You should have received a copy of the GNU Lesser General Public        *
16  * License along with this program (in the file LGPL.txt); if not,         *
17  * write to the Free Software Foundation, Inc., 51 Franklin St,            *
18  * Fifth Floor, Boston, MA  02110-1301  USA                                *
19  *                                                                         *
20  * This software consists of voluntary contributions made by many          *
21  * individuals.  For exact contribution history, see the revision          *
22  * history and logs, available at http://kdesvn.alwins-world.de.           *
23  ***************************************************************************/
24 #include "LogCache.h"
25 
26 #include <QDebug>
27 #include <QDir>
28 #include <QMap>
29 #include <QMutex>
30 #include <QThreadStorage>
31 #include <QSqlDatabase>
32 #include <QSqlError>
33 #include <QSqlQuery>
34 #include <QVariant>
35 
36 #include "svnqt/path.h"
37 #include "svnqt/cache/DatabaseException.h"
38 
SQLTYPE()39 static QString SQLTYPE() { return QStringLiteral("QSQLITE"); }
SQLMAIN()40 static QString SQLMAIN() { return QStringLiteral("logmain-logcache"); }
SQLMAINTABLE()41 static QString SQLMAINTABLE() { return QStringLiteral("logdb"); }
SQLTMPDB()42 static QString SQLTMPDB() { return QStringLiteral("tmpdb"); }
SQLREPOSPARAMETER()43 static QString SQLREPOSPARAMETER() { return QStringLiteral("repoparameter"); }
SQLSTATUS()44 static QString SQLSTATUS() { return QStringLiteral("logstatus"); }
45 
46 namespace svn
47 {
48 namespace cache
49 {
50 
51 LogCache *LogCache::mSelf = nullptr;
52 
53 class ThreadDBStore
54 {
55 public:
ThreadDBStore()56     ThreadDBStore()
57     {
58         m_DB = QSqlDatabase();
59     }
~ThreadDBStore()60     ~ThreadDBStore()
61     {
62         m_DB.commit();
63         m_DB.close();
64         m_DB = QSqlDatabase();
65         QMap<QString, QString>::Iterator it;
66         for (it = reposCacheNames.begin(); it != reposCacheNames.end(); ++it) {
67             if (QSqlDatabase::database(it.value()).isOpen()) {
68                 QSqlDatabase::database(it.value()).commit();
69                 QSqlDatabase::database(it.value()).close();
70             }
71             QSqlDatabase::removeDatabase(it.value());
72         }
73         QSqlDatabase::removeDatabase(key);
74     }
75 
deleteDb(const QString & path)76     void deleteDb(const QString &path)
77     {
78         QMap<QString, QString>::Iterator it;
79         for (it = reposCacheNames.begin(); it != reposCacheNames.end(); ++it) {
80             QSqlDatabase _db = QSqlDatabase::database(it.value());
81             if (_db.databaseName() == path) {
82                 qDebug() << "Removing database " << _db.databaseName() << endl;
83                 if (_db.isOpen()) {
84                     _db.commit();
85                     _db.close();
86                 }
87                 QSqlDatabase::removeDatabase(it.value());
88                 it = reposCacheNames.begin();
89             }
90         }
91     }
92     QSqlDatabase m_DB;
93     QString key;
94     QMap<QString, QString> reposCacheNames;
95 };
96 
97 class LogCacheData
98 {
99 
100 protected:
101     QMutex m_singleDbMutex;
102 
103 public:
LogCacheData()104     LogCacheData() {}
~LogCacheData()105     ~LogCacheData()
106     {
107         if (m_mainDB.hasLocalData()) {
108             m_mainDB.localData()->m_DB.close();
109             m_mainDB.setLocalData(0L);
110         }
111     }
112 
idToPath(const QString & id) const113     QString idToPath(const QString &id) const
114     {
115         return m_BasePath + QLatin1Char('/') + id + QLatin1String(".db");
116     }
117 
deleteRepository(const QString & aRepository)118     bool deleteRepository(const QString &aRepository)
119     {
120         const QString id = getReposId(aRepository);
121 
122         static const QString s_q(QLatin1String("delete from ") + SQLREPOSPARAMETER() + QLatin1String(" where id = ?"));
123         static const QString r_q(QLatin1String("delete from ") + SQLMAINTABLE() + QLatin1String(" where id = ?"));
124         QSqlDatabase mainDB = getMainDB();
125         if (!mainDB.isValid()) {
126             qWarning("Failed to open main database.");
127             return false;
128         }
129         qDebug() << m_mainDB.localData()->reposCacheNames;
130         m_mainDB.localData()->deleteDb(idToPath(id));
131         qDebug() << m_mainDB.localData()->reposCacheNames;
132         QFile fi(idToPath(id));
133         if (fi.exists()) {
134             if (!fi.remove()) {
135                 qWarning() << "Could not delete " << fi.fileName();
136                 return false;
137             }
138         }
139         qDebug() << "Removed " << fi.fileName() << endl;
140         mainDB.transaction();
141         QSqlQuery _q(mainDB);
142         _q.prepare(s_q);
143         _q.bindValue(0, id);
144         if (!_q.exec()) {
145             qDebug() << "Error delete value: " << _q.lastError().text() << "(" << _q.lastQuery() << ")";
146             _q.finish();
147             mainDB.rollback();
148             return false;
149         }
150         _q.prepare(r_q);
151         _q.bindValue(0, id);
152         if (!_q.exec()) {
153             qDebug() << "Error delete value: " << _q.lastError().text() << "(" << _q.lastQuery() << ")";
154             _q.finish();
155             mainDB.rollback();
156             return false;
157         }
158         mainDB.commit();
159         return true;
160     }
161 
checkReposDb(QSqlDatabase aDb)162     bool checkReposDb(QSqlDatabase aDb)
163     {
164         if (!aDb.open()) {
165             return false;
166         }
167 
168         QSqlQuery _q(aDb);
169         QStringList list = aDb.tables();
170 
171         aDb.transaction();
172         if (!list.contains(QStringLiteral("logentries"))) {
173             _q.exec(QStringLiteral("CREATE TABLE \"logentries\" (\"idx\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"revision\" INTEGER UNIQUE,\"date\" INTEGER,\"author\" TEXT, \"message\" TEXT)"));
174         }
175         if (!list.contains(QStringLiteral("changeditems"))) {
176             _q.exec(QStringLiteral("CREATE TABLE \"changeditems\" (\"idx\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"revision\" INTEGER,\"changeditem\" TEXT,\"action\" TEXT,\"copyfrom\" TEXT,\"copyfromrev\" INTEGER, UNIQUE(revision,changeditem,action))"));
177         }
178         if (!list.contains(QStringLiteral("mergeditems"))) {
179             _q.exec(QStringLiteral("CREATE TABLE \"mergeditems\" (\"revision\" INTEGER,\"mergeditems\" TEXT, PRIMARY KEY(revision))"));
180         }
181         if (!list.contains(QStringLiteral("dbversion"))) {
182             _q.exec(QStringLiteral("CREATE TABLE \"dbversion\" (\"version\" INTEGER)"));
183             qDebug() << _q.lastError();
184             _q.exec(QStringLiteral("INSERT INTO \"dbversion\" (version) VALUES(0)"));
185         }
186         aDb.commit();
187         list = aDb.tables();
188         if (!list.contains(QStringLiteral("logentries")) ||
189             !list.contains(QStringLiteral("changeditems")) ||
190             !list.contains(QStringLiteral("mergeditems")) ||
191             !list.contains(QStringLiteral("dbversion"))) {
192             qDebug() << "lists: " << list;
193             return false;
194         }
195         _q.exec(QStringLiteral("SELECT VERSION from dbversion limit 1"));
196         if (_q.lastError().type() == QSqlError::NoError && _q.next()) {
197             int version = _q.value(0).toInt();
198             if (version == 0) {
199                 _q.exec(QStringLiteral("create index if not exists main.authorindex on logentries(author)"));
200                 if (_q.lastError().type() != QSqlError::NoError) {
201                     qDebug() << _q.lastError();
202                 } else {
203                     _q.exec(QStringLiteral("UPDATE dbversion SET VERSION=1"));
204                 }
205                 ++version;
206             }
207             if (version == 1) {
208                 _q.exec(QStringLiteral("create index if not exists main.dateindex on logentries(date)"));
209                 if (_q.lastError().type() != QSqlError::NoError) {
210                     qDebug() << _q.lastError();
211                 } else {
212                     _q.exec(QStringLiteral("UPDATE dbversion SET VERSION=2"));
213                 }
214                 ++version;
215             }
216         } else {
217             qDebug() << "Select: " << _q.lastError();
218         }
219         return true;
220     }
221 
createReposDB(const svn::Path & reposroot)222     QString createReposDB(const svn::Path &reposroot)
223     {
224         QMutexLocker locker(&m_singleDbMutex);
225 
226         QSqlDatabase _mdb = getMainDB();
227 
228         _mdb.transaction();
229         QSqlQuery query(_mdb);
230         QString q(QLatin1String("insert into ") + SQLMAINTABLE() + QLatin1String(" (reposroot) VALUES('") + reposroot.path() + QLatin1String("')"));
231 
232         if (!query.exec(q)) {
233             return QString();
234         }
235 
236         _mdb.commit();
237         query.prepare(reposSelect());
238         query.bindValue(0, reposroot.native());
239         QString db;
240         if (query.exec() && query.next()) {
241             db = query.value(0).toString();
242         } else {
243             //qDebug() << "Error select_01: " << query.lastError().text() << "(" << query.lastQuery() << ")";
244         }
245         if (!db.isEmpty()) {
246             QString fulldb = idToPath(db);
247             QSqlDatabase _db = QSqlDatabase::addDatabase(SQLTYPE(), SQLTMPDB());
248             _db.setDatabaseName(fulldb);
249             if (!checkReposDb(_db)) {
250             }
251             QSqlDatabase::removeDatabase(SQLTMPDB());
252         }
253         return db;
254     }
255 
getReposId(const svn::Path & reposroot)256     QString getReposId(const svn::Path &reposroot)
257     {
258         if (!getMainDB().isValid()) {
259             return QString();
260         }
261         QSqlQuery c(getMainDB());
262         c.prepare(reposSelect());
263         c.bindValue(0, reposroot.native());
264 
265         // only the first one
266         if (c.exec() && c.next()) {
267             return c.value(0).toString();
268         }
269         return QString();
270     }
271 
getReposDB(const svn::Path & reposroot)272     QSqlDatabase getReposDB(const svn::Path &reposroot)
273     {
274         if (!getMainDB().isValid()) {
275             return QSqlDatabase();
276         }
277         QString dbFile = getReposId(reposroot);
278 
279         if (dbFile.isEmpty()) {
280             dbFile = createReposDB(reposroot);
281             if (dbFile.isEmpty()) {
282                 return QSqlDatabase();
283             }
284         }
285         if (m_mainDB.localData()->reposCacheNames.find(dbFile) != m_mainDB.localData()->reposCacheNames.end()) {
286             QSqlDatabase db = QSqlDatabase::database(m_mainDB.localData()->reposCacheNames.value(dbFile));
287             checkReposDb(db);
288             return db;
289         }
290         unsigned i = 0;
291         QString _key = dbFile;
292         while (QSqlDatabase::contains(_key)) {
293             _key = QStringLiteral("%1-%2").arg(dbFile).arg(i++);
294         }
295         const QString fulldb = idToPath(dbFile);
296         QSqlDatabase db = QSqlDatabase::addDatabase(SQLTYPE(), _key);
297         db.setDatabaseName(fulldb);
298         if (!checkReposDb(db)) {
299             db = QSqlDatabase();
300         } else {
301             m_mainDB.localData()->reposCacheNames[dbFile] = _key;
302         }
303         return db;
304     }
305 
getMainDB() const306     QSqlDatabase getMainDB()const
307     {
308         if (!m_mainDB.hasLocalData()) {
309             unsigned i = 0;
310             QString _key = SQLMAIN();
311             while (QSqlDatabase::contains(_key)) {
312                 _key = QStringLiteral("%1-%2").arg(SQLMAIN()).arg(i++);
313             }
314             QSqlDatabase db = QSqlDatabase::addDatabase(SQLTYPE(), _key);
315             db.setDatabaseName(m_BasePath + QLatin1String("/maindb.db"));
316             if (db.open()) {
317                 m_mainDB.setLocalData(new ThreadDBStore);
318                 m_mainDB.localData()->key = _key;
319                 m_mainDB.localData()->m_DB = db;
320             }
321         }
322         if (m_mainDB.hasLocalData()) {
323             return m_mainDB.localData()->m_DB;
324         } else {
325             return QSqlDatabase();
326         }
327     }
328     QString m_BasePath;
329 
330     mutable QThreadStorage<ThreadDBStore *> m_mainDB;
331 
reposSelect()332     static QString reposSelect()
333     {
334         return QStringLiteral("SELECT id from ") +
335             SQLMAINTABLE() +
336             QStringLiteral(" where reposroot=? ORDER by id DESC");
337     }
338 };
339 
340 /*!
341     \fn svn::cache::LogCache::LogCache()
342  */
LogCache()343 LogCache::LogCache()
344     : m_BasePath(QDir::homePath() + QLatin1String("/.svnqt"))
345 {
346     setupCachePath();
347 }
348 
LogCache(const QString & aBasePath)349 LogCache::LogCache(const QString &aBasePath)
350 {
351     delete mSelf;
352     mSelf = this;
353     if (aBasePath.isEmpty()) {
354         m_BasePath = QDir::homePath() + QLatin1String("/.svnqt");
355     } else {
356         m_BasePath = aBasePath;
357     }
358     setupCachePath();
359 }
360 
~LogCache()361 LogCache::~LogCache()
362 {
363 }
364 
365 /*!
366     \fn svn::cache::LogCache::setupCachePath()
367  */
setupCachePath()368 void LogCache::setupCachePath()
369 {
370     m_CacheData.reset(new LogCacheData);
371     m_CacheData->m_BasePath = m_BasePath;
372     QDir d;
373     if (!d.exists(m_BasePath)) {
374         d.mkdir(m_BasePath);
375     }
376     m_BasePath = m_BasePath + QLatin1Char('/') + QLatin1String("logcache");
377     if (!d.exists(m_BasePath)) {
378         d.mkdir(m_BasePath);
379     }
380     m_CacheData->m_BasePath = m_BasePath;
381     if (d.exists(m_BasePath)) {
382         setupMainDb();
383     }
384 }
385 
setupMainDb()386 void LogCache::setupMainDb()
387 {
388     QSqlDatabase mainDB = m_CacheData->getMainDB();
389     if (!mainDB.isValid()) {
390         qWarning("Failed to open main database.");
391     } else {
392         const QStringList list = mainDB.tables();
393         QSqlQuery q(mainDB);
394         if (!list.contains(SQLSTATUS())) {
395             mainDB.transaction();
396             if (q.exec(QLatin1String("CREATE TABLE \"") + SQLSTATUS() + QLatin1String("\" (\"key\" TEXT PRIMARY KEY NOT NULL, \"value\" TEXT);"))) {
397                 q.exec(QLatin1String("INSERT INTO \"") + SQLSTATUS() + QLatin1String("\" (key,value) values(\"version\",\"0\");"));
398             }
399             mainDB.commit();
400         }
401         int version = databaseVersion();
402         if (version == 0) {
403             mainDB.transaction();
404             if (!list.contains(SQLMAINTABLE())) {
405                 q.exec(QLatin1String("CREATE TABLE IF NOT EXISTS \"") + SQLMAINTABLE() + QLatin1String("\" (\"reposroot\" TEXT,\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL);"));
406             }/* else {
407                 q.exec("CREATE TABLE IF NOT EXISTS \""+QString(SQLMAINTABLE)+"new\" (\"reposroot\" TEXT,\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL);");
408                 q.exec("insert into \""+QString(SQLMAINTABLE)+"new\" select \"reposroot\",\"id\" from \""+QString(SQLMAINTABLE)+"\");");
409                 q.exec("drop table \""+QString(SQLMAINTABLE)+"\";");
410                 q.exec("alter table \""+QString(SQLMAINTABLE)+"new\" to \""+QString(SQLMAINTABLE)+"\";");
411             }*/
412             ++version;
413         }
414         if (version == 1) {
415             mainDB.transaction();
416             if (!q.exec(QLatin1String("CREATE TABLE IF NOT EXISTS \"") + SQLREPOSPARAMETER() +
417                         QLatin1String("\" (\"id\" INTEGER NOT NULL, \"parameter\" TEXT, \"value\" TEXT, PRIMARY KEY(\"id\",\"parameter\"));"))) {
418                 qDebug() << "Error create: " << q.lastError().text() << "(" << q.lastQuery() << ")";
419             }
420             mainDB.commit();
421             ++version;
422         }
423         databaseVersion(version);
424     }
425 }
426 
databaseVersion(int newversion)427 void LogCache::databaseVersion(int newversion)
428 {
429     QSqlDatabase mainDB = m_CacheData->getMainDB();
430     if (!mainDB.isValid()) {
431         return;
432     }
433     static const QString _qs(QLatin1String("update \"") + SQLSTATUS() + QLatin1String("\" SET value = ? WHERE \"key\" = \"version\""));
434     QSqlQuery cur(mainDB);
435     cur.prepare(_qs);
436     cur.bindValue(0, newversion);
437     if (!cur.exec()) {
438         qDebug() << "Error set version: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
439     }
440 }
441 
databaseVersion() const442 int LogCache::databaseVersion()const
443 {
444     QSqlDatabase mainDB = m_CacheData->getMainDB();
445     if (!mainDB.isValid()) {
446         return -1;
447     }
448     static const QString _qs(QLatin1String("select value from \"") + SQLSTATUS() + QLatin1String("\" WHERE \"key\" = \"version\""));
449     QSqlQuery cur(mainDB);
450     cur.prepare(_qs);
451     if (!cur.exec()) {
452         qDebug() << "Error select version: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
453         return -1;
454     }
455     if (cur.isActive() && cur.next()) {
456         //qDebug("Sel result: %s",_q.value(0).toString().toUtf8().data());
457         return cur.value(0).toInt();
458     }
459     return -1;
460 }
461 
getRepositoryParameter(const svn::Path & repository,const QString & key) const462 QVariant LogCache::getRepositoryParameter(const svn::Path &repository, const QString &key)const
463 {
464     QSqlDatabase mainDB = m_CacheData->getMainDB();
465     if (!mainDB.isValid()) {
466         return QVariant();
467     }
468     static const QString qs(QLatin1String("select \"value\",\"repoparameter\".\"parameter\" as \"key\" from \"") + SQLREPOSPARAMETER() +
469                             QLatin1String("\" INNER JOIN \"") + SQLMAINTABLE() + QLatin1String("\" ON (\"") + SQLREPOSPARAMETER() +
470                             QLatin1String("\".id = \"") + SQLMAINTABLE() + QLatin1String("\".id and \"") + SQLMAINTABLE() +
471                             QLatin1String("\".reposroot = ?)  WHERE \"parameter\" = ?;"));
472     QSqlQuery cur(mainDB);
473     cur.prepare(qs);
474     cur.bindValue(0, repository.native());
475     cur.bindValue(1, key);
476     if (!cur.exec()) {
477         qWarning() << "Error select: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
478         return QVariant();
479     }
480     if (cur.isActive() && cur.next()) {
481         return cur.value(0);
482     }
483     return QVariant();
484 }
485 
setRepositoryParameter(const svn::Path & repository,const QString & key,const QVariant & value)486 bool LogCache::setRepositoryParameter(const svn::Path &repository, const QString &key, const QVariant &value)
487 {
488     QSqlDatabase mainDB = m_CacheData->getMainDB();
489     if (!mainDB.isValid()) {
490         return false;
491     }
492     QString id = m_CacheData->getReposId(repository);
493     if (id.isEmpty()) {
494         return false;
495     }
496     static const QString qs(QLatin1String("INSERT OR REPLACE INTO \"") + SQLREPOSPARAMETER() +
497                             QLatin1String("\" (\"id\",\"parameter\",\"value\") values (\"%1\",\"%2\",?);"));
498     static const QString dqs(QLatin1String("DELETE FROM \"") + SQLREPOSPARAMETER() +
499                              QLatin1String("\" WHERE \"id\"=? and \"parameter\" = ?"));
500     mainDB.transaction();
501     QSqlQuery cur(mainDB);
502     if (value.isValid()) {
503         QString _qs = qs.arg(id,key);//.arg(value.toByteArray());
504         cur.prepare(_qs);
505         cur.bindValue(0, value);
506         if (!cur.exec()) {
507             qDebug() << "Error insert new value: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
508             cur.finish();
509             mainDB.rollback();
510             return false;
511         }
512     } else {
513         cur.prepare(dqs);
514         cur.bindValue(0, id);
515         cur.bindValue(1, key);
516         if (!cur.exec()) {
517             qDebug() << "Error delete value: " << cur.lastError().text() << "(" << cur.lastQuery() << ")";
518             cur.finish();
519             mainDB.rollback();
520             return false;
521         }
522     }
523     mainDB.commit();
524     return true;
525 }
526 
527 }
528 }
529 
530 /*!
531     \fn svn::cache::LogCache::self()
532  */
self()533 svn::cache::LogCache *svn::cache::LogCache::self()
534 {
535     if (!mSelf) {
536         mSelf = new LogCache();
537     }
538     return mSelf;
539 }
540 
541 /*!
542     \fn svn::cache::LogCache::reposDb()
543  */
reposDb(const QString & aRepository)544 QSqlDatabase  svn::cache::LogCache::reposDb(const QString &aRepository)
545 {
546 //    //qDebug("reposDB");
547     return m_CacheData->getReposDB(aRepository);
548 }
549 
550 /*!
551     \fn svn::cache::LogCache::cachedRepositories()const
552  */
cachedRepositories() const553 QStringList svn::cache::LogCache::cachedRepositories()const
554 {
555     static const QString s_q(QLatin1String("select \"reposroot\" from ") + SQLMAINTABLE() + QLatin1String(" order by reposroot"));
556     QSqlDatabase mainDB = m_CacheData->getMainDB();
557     QStringList _res;
558     if (!mainDB.isValid()) {
559         qWarning("Failed to open main database.");
560         return _res;
561     }
562     QSqlQuery cur(mainDB);
563     cur.prepare(s_q);
564     if (!cur.exec()) {
565         throw svn::cache::DatabaseException(QLatin1String("Could not retrieve values: ") + cur.lastError().text());
566         return _res;
567     }
568     while (cur.next()) {
569         _res.append(cur.value(0).toString());
570     }
571 
572     return _res;
573 }
574 
valid() const575 bool svn::cache::LogCache::valid()const
576 {
577     return m_CacheData->getMainDB().isValid();
578 }
579 
deleteRepository(const QString & aRepository)580 bool svn::cache::LogCache::deleteRepository(const QString &aRepository)
581 {
582     return m_CacheData->deleteRepository(aRepository);
583 }
584