desktop/src/common/syncjournaldb.cpp

2506 lines
86 KiB
C++

/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <QCryptographicHash>
#include <QFile>
#include <QLoggingCategory>
#include <QStringList>
#include <QElapsedTimer>
#include <QUrl>
#include <QDir>
#include <sqlite3.h>
#include <cstring>
#include "common/syncjournaldb.h"
#include "version.h"
#include "filesystembase.h"
#include "common/asserts.h"
#include "common/checksums.h"
#include "common/preparedsqlquerymanager.h"
#include "common/c_jhash.h"
// SQL expression to check whether path.startswith(prefix + '/')
// Note: '/' + 1 == '0'
#define IS_PREFIX_PATH_OF(prefix, path) \
"(" path " > (" prefix "||'/') AND " path " < (" prefix "||'0'))"
#define IS_PREFIX_PATH_OR_EQUAL(prefix, path) \
"(" path " == " prefix " OR " IS_PREFIX_PATH_OF(prefix, path) ")"
namespace OCC {
Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg)
#define GET_FILE_RECORD_QUERY \
"SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \
" ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \
" lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap " \
" FROM metadata" \
" LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &query)
{
rec._path = query.baValue(0);
rec._inode = query.int64Value(1);
rec._modtime = query.int64Value(2);
rec._type = static_cast<ItemType>(query.intValue(3));
rec._etag = query.baValue(4);
rec._fileId = query.baValue(5);
rec._remotePerm = RemotePermissions::fromDbValue(query.baValue(6));
rec._fileSize = query.int64Value(7);
rec._serverHasIgnoredFiles = (query.intValue(8) > 0);
rec._checksumHeader = query.baValue(9);
rec._e2eMangledName = query.baValue(10);
rec._isE2eEncrypted = query.intValue(11) > 0;
rec._lockstate._locked = query.intValue(12) > 0;
rec._lockstate._lockOwnerDisplayName = query.stringValue(13);
rec._lockstate._lockOwnerId = query.stringValue(14);
rec._lockstate._lockOwnerType = query.int64Value(15);
rec._lockstate._lockEditorApp = query.stringValue(16);
rec._lockstate._lockTime = query.int64Value(17);
rec._lockstate._lockTimeout = query.int64Value(18);
rec._isShared = query.intValue(19) > 0;
rec._lastShareStateFetchedTimestmap = query.int64Value(20);
}
static QByteArray defaultJournalMode(const QString &dbPath)
{
#if defined(Q_OS_WIN)
// See #2693: Some exFAT file systems seem unable to cope with the
// WAL journaling mode. They work fine with DELETE.
QString fileSystem = FileSystem::fileSystemForPath(dbPath);
qCInfo(lcDb) << "Detected filesystem" << fileSystem << "for" << dbPath;
if (fileSystem.contains(QLatin1String("FAT"))) {
qCInfo(lcDb) << "Filesystem contains FAT - using DELETE journal mode";
return "DELETE";
}
#elif defined(Q_OS_MAC)
if (dbPath.startsWith(QLatin1String("/Volumes/"))) {
qCInfo(lcDb) << "Mounted sync dir, do not use WAL for" << dbPath;
return "DELETE";
}
#else
Q_UNUSED(dbPath)
#endif
return "WAL";
}
SyncJournalDb::SyncJournalDb(const QString &dbFilePath, QObject *parent)
: QObject(parent)
, _dbFile(dbFilePath)
, _transaction(0)
, _metadataTableIsEmpty(false)
{
// Allow forcing the journal mode for debugging
static QByteArray envJournalMode = qgetenv("OWNCLOUD_SQLITE_JOURNAL_MODE");
_journalMode = envJournalMode;
if (_journalMode.isEmpty()) {
_journalMode = defaultJournalMode(_dbFile);
}
}
QString SyncJournalDb::makeDbName(const QString &localPath,
const QUrl &remoteUrl,
const QString &remotePath,
const QString &user)
{
QString journalPath = QStringLiteral(".sync_");
QString key = QStringLiteral("%1@%2:%3").arg(user, remoteUrl.toString(), remotePath);
QByteArray ba = QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Md5);
journalPath += QString::fromLatin1(ba.left(6).toHex()) + QStringLiteral(".db");
// If it exists already, the path is clearly usable
QFile file(QDir(localPath).filePath(journalPath));
if (file.exists()) {
return journalPath;
}
// Try to create a file there
if (file.open(QIODevice::ReadWrite)) {
// Ok, all good.
file.close();
file.remove();
return journalPath;
}
// Error during creation, just keep the original and throw errors later
qCWarning(lcDb) << "Could not find a writable database path" << file.fileName() << file.errorString();
return journalPath;
}
bool SyncJournalDb::maybeMigrateDb(const QString &localPath, const QString &absoluteJournalPath)
{
const QString oldDbName = localPath + QLatin1String(".csync_journal.db");
if (!FileSystem::fileExists(oldDbName)) {
return true;
}
const QString oldDbNameShm = oldDbName + QStringLiteral("-shm");
const QString oldDbNameWal = oldDbName + QStringLiteral("-wal");
const QString newDbName = absoluteJournalPath;
const QString newDbNameShm = newDbName + QStringLiteral("-shm");
const QString newDbNameWal = newDbName + QStringLiteral("-wal");
// Whenever there is an old db file, migrate it to the new db path.
// This is done to make switching from older versions to newer versions
// work correctly even if the user had previously used a new version
// and therefore already has an (outdated) new-style db file.
QString error;
if (FileSystem::fileExists(newDbName)) {
if (!FileSystem::remove(newDbName, &error)) {
qCWarning(lcDb) << "Database migration: Could not remove db file" << newDbName
<< "due to" << error;
return false;
}
}
if (FileSystem::fileExists(newDbNameWal)) {
if (!FileSystem::remove(newDbNameWal, &error)) {
qCWarning(lcDb) << "Database migration: Could not remove db WAL file" << newDbNameWal
<< "due to" << error;
return false;
}
}
if (FileSystem::fileExists(newDbNameShm)) {
if (!FileSystem::remove(newDbNameShm, &error)) {
qCWarning(lcDb) << "Database migration: Could not remove db SHM file" << newDbNameShm
<< "due to" << error;
return false;
}
}
if (!FileSystem::rename(oldDbName, newDbName, &error)) {
qCWarning(lcDb) << "Database migration: could not rename" << oldDbName
<< "to" << newDbName << ":" << error;
return false;
}
if (!FileSystem::rename(oldDbNameWal, newDbNameWal, &error)) {
qCWarning(lcDb) << "Database migration: could not rename" << oldDbNameWal
<< "to" << newDbNameWal << ":" << error;
return false;
}
if (!FileSystem::rename(oldDbNameShm, newDbNameShm, &error)) {
qCWarning(lcDb) << "Database migration: could not rename" << oldDbNameShm
<< "to" << newDbNameShm << ":" << error;
return false;
}
qCInfo(lcDb) << "Journal successfully migrated from" << oldDbName << "to" << newDbName;
return true;
}
bool SyncJournalDb::exists()
{
QMutexLocker locker(&_mutex);
return (!_dbFile.isEmpty() && QFile::exists(_dbFile));
}
QString SyncJournalDb::databaseFilePath() const
{
return _dbFile;
}
// Note that this does not change the size of the -wal file, but it is supposed to make
// the normal .db faster since the changes from the wal will be incorporated into it.
// Then the next sync (and the SocketAPI) will have a faster access.
void SyncJournalDb::walCheckpoint()
{
QElapsedTimer t;
t.start();
SqlQuery pragma1(_db);
pragma1.prepare("PRAGMA wal_checkpoint(FULL);");
if (pragma1.exec()) {
qCDebug(lcDb) << "took" << t.elapsed() << "msec";
}
}
void SyncJournalDb::startTransaction()
{
if (_transaction == 0) {
if (!_db.transaction()) {
qCWarning(lcDb) << "ERROR starting transaction:" << _db.error();
return;
}
_transaction = 1;
} else {
qCDebug(lcDb) << "Database Transaction is running, not starting another one!";
}
}
void SyncJournalDb::commitTransaction()
{
if (_transaction == 1) {
if (!_db.commit()) {
qCWarning(lcDb) << "ERROR committing to the database:" << _db.error();
return;
}
_transaction = 0;
} else {
qCDebug(lcDb) << "No database Transaction to commit";
}
}
bool SyncJournalDb::sqlFail(const QString &log, const SqlQuery &query)
{
commitTransaction();
qCWarning(lcDb) << "SQL Error" << log << query.error();
_db.close();
ASSERT(false);
return false;
}
bool SyncJournalDb::checkConnect()
{
if (autotestFailCounter >= 0) {
if (!autotestFailCounter--) {
qCInfo(lcDb) << "Error Simulated";
return false;
}
}
if (_db.isOpen()) {
// Unfortunately the sqlite isOpen check can return true even when the underlying storage
// has become unavailable - and then some operations may cause crashes. See #6049
if (!QFile::exists(_dbFile)) {
qCWarning(lcDb) << "Database open, but file" << _dbFile << "does not exist";
close();
return false;
}
return true;
}
if (_dbFile.isEmpty()) {
qCWarning(lcDb) << "Database filename" << _dbFile << "is empty";
return false;
}
// The database file is created by this call (SQLITE_OPEN_CREATE)
if (!_db.openOrCreateReadWrite(_dbFile)) {
QString error = _db.error();
qCWarning(lcDb) << "Error opening the db:" << error;
return false;
}
if (!QFile::exists(_dbFile)) {
qCWarning(lcDb) << "Database file" << _dbFile << "does not exist";
return false;
}
SqlQuery pragma1(_db);
pragma1.prepare("SELECT sqlite_version();");
if (!pragma1.exec()) {
return sqlFail(QStringLiteral("SELECT sqlite_version()"), pragma1);
} else {
pragma1.next();
qCInfo(lcDb) << "sqlite3 version" << pragma1.stringValue(0);
}
// Set locking mode to avoid issues with WAL on Windows
static QByteArray locking_mode_env = qgetenv("OWNCLOUD_SQLITE_LOCKING_MODE");
if (locking_mode_env.isEmpty())
locking_mode_env = "EXCLUSIVE";
pragma1.prepare("PRAGMA locking_mode=" + locking_mode_env + ";");
if (!pragma1.exec()) {
return sqlFail(QStringLiteral("Set PRAGMA locking_mode"), pragma1);
} else {
pragma1.next();
qCInfo(lcDb) << "sqlite3 locking_mode=" << pragma1.stringValue(0);
}
pragma1.prepare("PRAGMA journal_mode=" + _journalMode + ";");
if (!pragma1.exec()) {
return sqlFail(QStringLiteral("Set PRAGMA journal_mode"), pragma1);
} else {
pragma1.next();
qCInfo(lcDb) << "sqlite3 journal_mode=" << pragma1.stringValue(0);
}
// For debugging purposes, allow temp_store to be set
static QByteArray env_temp_store = qgetenv("OWNCLOUD_SQLITE_TEMP_STORE");
if (!env_temp_store.isEmpty()) {
pragma1.prepare("PRAGMA temp_store = " + env_temp_store + ";");
if (!pragma1.exec()) {
return sqlFail(QStringLiteral("Set PRAGMA temp_store"), pragma1);
}
qCInfo(lcDb) << "sqlite3 with temp_store =" << env_temp_store;
}
// With WAL journal the NORMAL sync mode is safe from corruption,
// otherwise use the standard FULL mode.
QByteArray synchronousMode = "FULL";
if (QString::fromUtf8(_journalMode).compare(QStringLiteral("wal"), Qt::CaseInsensitive) == 0)
synchronousMode = "NORMAL";
pragma1.prepare("PRAGMA synchronous = " + synchronousMode + ";");
if (!pragma1.exec()) {
return sqlFail(QStringLiteral("Set PRAGMA synchronous"), pragma1);
} else {
qCInfo(lcDb) << "sqlite3 synchronous=" << synchronousMode;
}
pragma1.prepare("PRAGMA case_sensitive_like = ON;");
if (!pragma1.exec()) {
return sqlFail(QStringLiteral("Set PRAGMA case_sensitivity"), pragma1);
}
sqlite3_create_function(_db.sqliteDb(), "parent_hash", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr,
[] (sqlite3_context *ctx,int, sqlite3_value **argv) {
auto text = reinterpret_cast<const char*>(sqlite3_value_text(argv[0]));
const char *end = std::strrchr(text, '/');
if (!end) end = text;
sqlite3_result_int64(ctx, c_jhash64(reinterpret_cast<const uint8_t*>(text),
end - text, 0));
}, nullptr, nullptr);
/* Because insert is so slow, we do everything in a transaction, and only need one call to commit */
startTransaction();
SqlQuery createQuery(_db);
createQuery.prepare("CREATE TABLE IF NOT EXISTS metadata("
"phash INTEGER(8),"
"pathlen INTEGER,"
"path VARCHAR(4096),"
"inode INTEGER,"
"uid INTEGER,"
"gid INTEGER,"
"mode INTEGER,"
"modtime INTEGER(8),"
"type INTEGER,"
"md5 VARCHAR(32)," /* This is the etag. Called md5 for compatibility */
// updateDatabaseStructure() will add
// fileid
// remotePerm
// filesize
// ignoredChildrenRemote
// contentChecksum
// contentChecksumTypeId
"PRIMARY KEY(phash)"
");");
#ifndef SQLITE_IOERR_SHMMAP
// Requires sqlite >= 3.7.7 but old CentOS6 has sqlite-3.6.20
// Definition taken from https://sqlite.org/c3ref/c_abort_rollback.html
#define SQLITE_IOERR_SHMMAP (SQLITE_IOERR | (21<<8))
#endif
if (!createQuery.exec()) {
// In certain situations the io error can be avoided by switching
// to the DELETE journal mode, see #5723
if (_journalMode != "DELETE"
&& createQuery.errorId() == SQLITE_IOERR
&& sqlite3_extended_errcode(_db.sqliteDb()) == SQLITE_IOERR_SHMMAP) {
qCWarning(lcDb) << "IO error SHMMAP on table creation, attempting with DELETE journal mode";
_journalMode = "DELETE";
commitTransaction();
_db.close();
return checkConnect();
}
return sqlFail(QStringLiteral("Create table metadata"), createQuery);
}
createQuery.prepare("CREATE TABLE IF NOT EXISTS key_value_store(key VARCHAR(4096), value VARCHAR(4096), PRIMARY KEY(key));");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table key_value_store"), createQuery);
}
createQuery.prepare("CREATE TABLE IF NOT EXISTS downloadinfo("
"path VARCHAR(4096),"
"tmpfile VARCHAR(4096),"
"etag VARCHAR(32),"
"errorcount INTEGER,"
"PRIMARY KEY(path)"
");");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table downloadinfo"), createQuery);
}
createQuery.prepare("CREATE TABLE IF NOT EXISTS uploadinfo("
"path VARCHAR(4096),"
"chunk INTEGER,"
"transferid INTEGER,"
"errorcount INTEGER,"
"size INTEGER(8),"
"modtime INTEGER(8),"
"contentChecksum TEXT,"
"PRIMARY KEY(path)"
");");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table uploadinfo"), createQuery);
}
// create the blacklist table.
createQuery.prepare("CREATE TABLE IF NOT EXISTS blacklist ("
"path VARCHAR(4096),"
"lastTryEtag VARCHAR[32],"
"lastTryModtime INTEGER[8],"
"retrycount INTEGER,"
"errorstring VARCHAR[4096],"
"PRIMARY KEY(path)"
");");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table blacklist"), createQuery);
}
createQuery.prepare("CREATE TABLE IF NOT EXISTS async_poll("
"path VARCHAR(4096),"
"modtime INTEGER(8),"
"filesize BIGINT,"
"pollpath VARCHAR(4096));");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table async_poll"), createQuery);
}
// create the selectivesync table.
createQuery.prepare("CREATE TABLE IF NOT EXISTS selectivesync ("
"path VARCHAR(4096),"
"type INTEGER"
");");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table selectivesync"), createQuery);
}
// create the checksumtype table.
createQuery.prepare("CREATE TABLE IF NOT EXISTS checksumtype("
"id INTEGER PRIMARY KEY,"
"name TEXT UNIQUE"
");");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table checksumtype"), createQuery);
}
// create the datafingerprint table.
createQuery.prepare("CREATE TABLE IF NOT EXISTS datafingerprint("
"fingerprint TEXT UNIQUE"
");");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table datafingerprint"), createQuery);
}
// create the flags table.
createQuery.prepare("CREATE TABLE IF NOT EXISTS flags ("
"path TEXT PRIMARY KEY,"
"pinState INTEGER"
");");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table flags"), createQuery);
}
// create the conflicts table.
createQuery.prepare("CREATE TABLE IF NOT EXISTS conflicts("
"path TEXT PRIMARY KEY,"
"baseFileId TEXT,"
"baseEtag TEXT,"
"baseModtime INTEGER"
");");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table conflicts"), createQuery);
}
createQuery.prepare("CREATE TABLE IF NOT EXISTS version("
"major INTEGER(8),"
"minor INTEGER(8),"
"patch INTEGER(8),"
"custom VARCHAR(256)"
");");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table version"), createQuery);
}
bool forceRemoteDiscovery = false;
SqlQuery versionQuery("SELECT major, minor, patch FROM version;", _db);
if (!versionQuery.next().hasData) {
forceRemoteDiscovery = true;
createQuery.prepare("INSERT INTO version VALUES (?1, ?2, ?3, ?4);");
createQuery.bindValue(1, MIRALL_VERSION_MAJOR);
createQuery.bindValue(2, MIRALL_VERSION_MINOR);
createQuery.bindValue(3, MIRALL_VERSION_PATCH);
createQuery.bindValue(4, MIRALL_VERSION_BUILD);
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Update version"), createQuery);
}
} else {
int major = versionQuery.intValue(0);
int minor = versionQuery.intValue(1);
int patch = versionQuery.intValue(2);
if (major == 1 && minor == 8 && (patch == 0 || patch == 1)) {
qCInfo(lcDb) << "possibleUpgradeFromMirall_1_8_0_or_1 detected!";
forceRemoteDiscovery = true;
}
// There was a bug in versions <2.3.0 that could lead to stale
// local files and a remote discovery will fix them.
// See #5190 #5242.
if (major == 2 && minor < 3) {
qCInfo(lcDb) << "upgrade form client < 2.3.0 detected! forcing remote discovery";
forceRemoteDiscovery = true;
}
// Not comparing the BUILD id here, correct?
if (!(major == MIRALL_VERSION_MAJOR && minor == MIRALL_VERSION_MINOR && patch == MIRALL_VERSION_PATCH)) {
createQuery.prepare("UPDATE version SET major=?1, minor=?2, patch =?3, custom=?4 "
"WHERE major=?5 AND minor=?6 AND patch=?7;");
createQuery.bindValue(1, MIRALL_VERSION_MAJOR);
createQuery.bindValue(2, MIRALL_VERSION_MINOR);
createQuery.bindValue(3, MIRALL_VERSION_PATCH);
createQuery.bindValue(4, MIRALL_VERSION_BUILD);
createQuery.bindValue(5, major);
createQuery.bindValue(6, minor);
createQuery.bindValue(7, patch);
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Update version"), createQuery);
}
}
}
commitInternal(QStringLiteral("checkConnect"));
bool rc = updateDatabaseStructure();
if (!rc) {
qCWarning(lcDb) << "Failed to update the database structure!";
}
/*
* If we are upgrading from a client version older than 1.5,
* we cannot read from the database because we need to fetch the files id and etags.
*
* If 1.8.0 caused missing data in the local tree, so we also don't read from DB
* to get back the files that were gone.
* In 1.8.1 we had a fix to re-get the data, but this one here is better
*/
if (forceRemoteDiscovery) {
forceRemoteDiscoveryNextSyncLocked();
}
const auto deleteDownloadInfo = _queryManager.get(PreparedSqlQueryManager::DeleteDownloadInfoQuery, QByteArrayLiteral("DELETE FROM downloadinfo WHERE path=?1"), _db);
if (!deleteDownloadInfo) {
return sqlFail(QStringLiteral("prepare _deleteDownloadInfoQuery"), *deleteDownloadInfo);
}
const auto deleteUploadInfoQuery = _queryManager.get(PreparedSqlQueryManager::DeleteUploadInfoQuery, QByteArrayLiteral("DELETE FROM uploadinfo WHERE path=?1"), _db);
if (!deleteUploadInfoQuery) {
return sqlFail(QStringLiteral("prepare _deleteUploadInfoQuery"), *deleteUploadInfoQuery);
}
QByteArray sql("SELECT lastTryEtag, lastTryModtime, retrycount, errorstring, lastTryTime, ignoreDuration, renameTarget, errorCategory, requestId "
"FROM blacklist WHERE path=?1");
if (Utility::fsCasePreserving()) {
// if the file system is case preserving we have to check the blacklist
// case insensitively
sql += " COLLATE NOCASE";
}
const auto getErrorBlacklistQuery = _queryManager.get(PreparedSqlQueryManager::GetErrorBlacklistQuery, sql, _db);
if (!getErrorBlacklistQuery) {
return sqlFail(QStringLiteral("prepare _getErrorBlacklistQuery"), *getErrorBlacklistQuery);
}
// don't start a new transaction now
commitInternal(QStringLiteral("checkConnect End"), false);
// This avoid reading from the DB if we already know it is empty
// thereby speeding up the initial discovery significantly.
_metadataTableIsEmpty = (getFileRecordCount() == 0);
// Hide 'em all!
FileSystem::setFileHidden(databaseFilePath(), true);
FileSystem::setFileHidden(databaseFilePath() + QStringLiteral("-wal"), true);
FileSystem::setFileHidden(databaseFilePath() + QStringLiteral("-shm"), true);
FileSystem::setFileHidden(databaseFilePath() + QStringLiteral("-journal"), true);
return rc;
}
void SyncJournalDb::close()
{
QMutexLocker locker(&_mutex);
qCInfo(lcDb) << "Closing DB" << _dbFile;
commitTransaction();
_db.close();
clearEtagStorageFilter();
_metadataTableIsEmpty = false;
}
bool SyncJournalDb::updateDatabaseStructure()
{
if (!updateMetadataTableStructure())
return false;
if (!updateErrorBlacklistTableStructure())
return false;
return true;
}
bool SyncJournalDb::updateMetadataTableStructure()
{
auto columns = tableColumns("metadata");
bool re = true;
// check if the file_id column is there and create it if not
if (columns.isEmpty()) {
return false;
}
const auto addColumn = [this, &columns, &re] (const QString &columnName, const QString &dataType, const bool withIndex = false) {
const auto latin1ColumnName = columnName.toLatin1();
if (columns.indexOf(latin1ColumnName) == -1) {
SqlQuery query(_db);
const auto request = QStringLiteral("ALTER TABLE metadata ADD COLUMN %1 %2;").arg(columnName).arg(dataType);
query.prepare(request.toLatin1());
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: add %1 column").arg(columnName), query);
re = false;
}
if (withIndex) {
query.prepare(QStringLiteral("CREATE INDEX metadata_%1 ON metadata(%1);").arg(columnName).toLatin1());
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: create index %1").arg(columnName), query);
re = false;
}
}
commitInternal(QStringLiteral("update database structure: add %1 column").arg(columnName));
}
};
addColumn(QStringLiteral("fileid"), QStringLiteral("VARCHAR(128)"), true);
addColumn(QStringLiteral("remotePerm"), QStringLiteral("VARCHAR(128)"));
addColumn(QStringLiteral("filesize"), QStringLiteral("BIGINT"));
if (true) {
SqlQuery query(_db);
query.prepare("CREATE INDEX IF NOT EXISTS metadata_inode ON metadata(inode);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: create index inode"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add inode index"));
}
if (true) {
SqlQuery query(_db);
query.prepare("CREATE INDEX IF NOT EXISTS metadata_path ON metadata(path);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: create index path"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add path index"));
}
if (true) {
SqlQuery query(_db);
query.prepare("CREATE INDEX IF NOT EXISTS metadata_parent ON metadata(parent_hash(path));");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: create index parent"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add parent index"));
}
addColumn(QStringLiteral("ignoredChildrenRemote"), QStringLiteral("INT"));
addColumn(QStringLiteral("contentChecksum"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("contentChecksumTypeId"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("e2eMangledName"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("isShared"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("lastShareStateFetchedTimestmap"), QStringLiteral("INTEGER"));
auto uploadInfoColumns = tableColumns("uploadinfo");
if (uploadInfoColumns.isEmpty())
return false;
if (!uploadInfoColumns.contains("contentChecksum")) {
SqlQuery query(_db);
query.prepare("ALTER TABLE uploadinfo ADD COLUMN contentChecksum TEXT;");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: add contentChecksum column"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add contentChecksum col for uploadinfo"));
}
auto conflictsColumns = tableColumns("conflicts");
if (conflictsColumns.isEmpty())
return false;
if (!conflictsColumns.contains("basePath")) {
SqlQuery query(_db);
query.prepare("ALTER TABLE conflicts ADD COLUMN basePath TEXT;");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: add basePath column"), query);
re = false;
}
}
if (true) {
SqlQuery query(_db);
query.prepare("CREATE INDEX IF NOT EXISTS metadata_e2e_id ON metadata(e2eMangledName);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateMetadataTableStructure: create index e2eMangledName"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add e2eMangledName index"));
}
addColumn(QStringLiteral("lock"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("lockType"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("lockOwnerDisplayName"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("lockOwnerId"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("lockOwnerEditor"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("lockTime"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("lockTimeout"), QStringLiteral("INTEGER"));
return re;
}
bool SyncJournalDb::updateErrorBlacklistTableStructure()
{
auto columns = tableColumns("blacklist");
bool re = true;
if (columns.isEmpty()) {
return false;
}
if (columns.indexOf("lastTryTime") == -1) {
SqlQuery query(_db);
query.prepare("ALTER TABLE blacklist ADD COLUMN lastTryTime INTEGER(8);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateBlacklistTableStructure: Add lastTryTime fileid"), query);
re = false;
}
query.prepare("ALTER TABLE blacklist ADD COLUMN ignoreDuration INTEGER(8);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateBlacklistTableStructure: Add ignoreDuration fileid"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add lastTryTime, ignoreDuration cols"));
}
if (columns.indexOf("renameTarget") == -1) {
SqlQuery query(_db);
query.prepare("ALTER TABLE blacklist ADD COLUMN renameTarget VARCHAR(4096);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateBlacklistTableStructure: Add renameTarget"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add renameTarget col"));
}
if (columns.indexOf("errorCategory") == -1) {
SqlQuery query(_db);
query.prepare("ALTER TABLE blacklist ADD COLUMN errorCategory INTEGER(8);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateBlacklistTableStructure: Add errorCategory"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add errorCategory col"));
}
if (columns.indexOf("requestId") == -1) {
SqlQuery query(_db);
query.prepare("ALTER TABLE blacklist ADD COLUMN requestId VARCHAR(36);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateBlacklistTableStructure: Add requestId"), query);
re = false;
}
commitInternal(QStringLiteral("update database structure: add errorCategory col"));
}
SqlQuery query(_db);
query.prepare("CREATE INDEX IF NOT EXISTS blacklist_index ON blacklist(path collate nocase);");
if (!query.exec()) {
sqlFail(QStringLiteral("updateErrorBlacklistTableStructure: create index blacklit"), query);
re = false;
}
return re;
}
QVector<QByteArray> SyncJournalDb::tableColumns(const QByteArray &table)
{
QVector<QByteArray> columns;
if (!checkConnect()) {
return columns;
}
SqlQuery query("PRAGMA table_info('" + table + "');", _db);
if (!query.exec()) {
return columns;
}
while (query.next().hasData) {
columns.append(query.baValue(1));
}
qCDebug(lcDb) << "Columns in the current journal:" << columns;
return columns;
}
qint64 SyncJournalDb::getPHash(const QByteArray &file)
{
qint64 h = 0;
int len = file.length();
h = c_jhash64((uint8_t *)file.data(), len, 0);
return h;
}
Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &_record)
{
SyncJournalFileRecord record = _record;
QMutexLocker locker(&_mutex);
if (!_etagStorageFilter.isEmpty()) {
// If we are a directory that should not be read from db next time, don't write the etag
QByteArray prefix = record._path + "/";
foreach (const QByteArray &it, _etagStorageFilter) {
if (it.startsWith(prefix)) {
qCInfo(lcDb) << "Filtered writing the etag of" << prefix << "because it is a prefix of" << it;
record._etag = "_invalid_";
break;
}
}
}
qCInfo(lcDb) << "Updating file record for path:" << record.path() << "inode:" << record._inode
<< "modtime:" << record._modtime << "type:" << record._type << "etag:" << record._etag
<< "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString()
<< "fileSize:" << record._fileSize << "checksum:" << record._checksumHeader
<< "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record._isE2eEncrypted
<< "lock:" << (record._lockstate._locked ? "true" : "false")
<< "lock owner type:" << record._lockstate._lockOwnerType
<< "lock owner:" << record._lockstate._lockOwnerDisplayName
<< "lock owner id:" << record._lockstate._lockOwnerId
<< "lock editor:" << record._lockstate._lockEditorApp
<< "isShared:" << record._isShared
<< "lastShareStateFetchedTimestmap:" << record._lastShareStateFetchedTimestmap;
const qint64 phash = getPHash(record._path);
if (!checkConnect()) {
qCWarning(lcDb) << "Failed to connect database.";
return tr("Failed to connect database."); // checkConnect failed.
}
int plen = record._path.length();
QByteArray etag(record._etag);
if (etag.isEmpty()) {
etag = "";
}
QByteArray fileId(record._fileId);
if (fileId.isEmpty()) {
fileId = "";
}
QByteArray remotePerm = record._remotePerm.toDbValue();
QByteArray checksumType, checksum;
parseChecksumHeader(record._checksumHeader, &checksumType, &checksum);
int contentChecksumTypeId = mapChecksumType(checksumType);
const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO metadata "
"(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, "
"contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwnerDisplayName, lockOwnerId, "
"lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap) "
"VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27);"),
_db);
if (!query) {
return query->error();
}
query->bindValue(1, phash);
query->bindValue(2, plen);
query->bindValue(3, record._path);
query->bindValue(4, record._inode);
query->bindValue(5, 0); // uid Not used
query->bindValue(6, 0); // gid Not used
query->bindValue(7, 0); // mode Not used
query->bindValue(8, record._modtime);
query->bindValue(9, record._type);
query->bindValue(10, etag);
query->bindValue(11, fileId);
query->bindValue(12, remotePerm);
query->bindValue(13, record._fileSize);
query->bindValue(14, record._serverHasIgnoredFiles ? 1 : 0);
query->bindValue(15, checksum);
query->bindValue(16, contentChecksumTypeId);
query->bindValue(17, record._e2eMangledName);
query->bindValue(18, record._isE2eEncrypted);
query->bindValue(19, record._lockstate._locked ? 1 : 0);
query->bindValue(20, record._lockstate._lockOwnerType);
query->bindValue(21, record._lockstate._lockOwnerDisplayName);
query->bindValue(22, record._lockstate._lockOwnerId);
query->bindValue(23, record._lockstate._lockEditorApp);
query->bindValue(24, record._lockstate._lockTime);
query->bindValue(25, record._lockstate._lockTimeout);
query->bindValue(26, record._isShared);
query->bindValue(27, record._lastShareStateFetchedTimestmap);
if (!query->exec()) {
return query->error();
}
// Can't be true anymore.
_metadataTableIsEmpty = false;
return {};
}
void SyncJournalDb::keyValueStoreSet(const QString &key, QVariant value)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return;
}
const auto query = _queryManager.get(PreparedSqlQueryManager::SetKeyValueStoreQuery, QByteArrayLiteral("INSERT OR REPLACE INTO key_value_store (key, value) VALUES(?1, ?2);"), _db);
if (!query) {
return;
}
query->bindValue(1, key);
query->bindValue(2, value);
query->exec();
}
qint64 SyncJournalDb::keyValueStoreGetInt(const QString &key, qint64 defaultValue)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return defaultValue;
}
const auto query = _queryManager.get(PreparedSqlQueryManager::GetKeyValueStoreQuery, QByteArrayLiteral("SELECT value FROM key_value_store WHERE key=?1"), _db);
if (!query) {
return defaultValue;
}
query->bindValue(1, key);
query->exec();
auto result = query->next();
if (!result.ok || !result.hasData) {
return defaultValue;
}
return query->int64Value(0);
}
void SyncJournalDb::keyValueStoreDelete(const QString &key)
{
const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteKeyValueStoreQuery, QByteArrayLiteral("DELETE FROM key_value_store WHERE key=?1;"), _db);
if (!query) {
qCWarning(lcDb) << "Failed to initOrReset _deleteKeyValueStoreQuery";
Q_ASSERT(false);
}
query->bindValue(1, key);
if (!query->exec()) {
qCWarning(lcDb) << "Failed to exec _deleteKeyValueStoreQuery for key" << key;
Q_ASSERT(false);
}
}
// TODO: filename -> QBytearray?
bool SyncJournalDb::deleteFileRecord(const QString &filename, bool recursively)
{
QMutexLocker locker(&_mutex);
if (checkConnect()) {
// if (!recursively) {
// always delete the actual file.
{
const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteFileRecordPhash, QByteArrayLiteral("DELETE FROM metadata WHERE phash=?1"), _db);
if (!query) {
return false;
}
const qint64 phash = getPHash(filename.toUtf8());
query->bindValue(1, phash);
if (!query->exec()) {
return false;
}
}
if (recursively) {
const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteFileRecordRecursively, QByteArrayLiteral("DELETE FROM metadata WHERE " IS_PREFIX_PATH_OF("?1", "path")), _db);
if (!query)
return false;
query->bindValue(1, filename);
if (!query->exec()) {
return false;
}
}
return true;
} else {
qCWarning(lcDb) << "Failed to connect database.";
return false; // checkConnect failed.
}
}
bool SyncJournalDb::getFileRecord(const QByteArray &filename, SyncJournalFileRecord *rec)
{
QMutexLocker locker(&_mutex);
// Reset the output var in case the caller is reusing it.
Q_ASSERT(rec);
rec->_path.clear();
Q_ASSERT(!rec->isValid());
if (_metadataTableIsEmpty)
return true; // no error, yet nothing found (rec->isValid() == false)
if (!checkConnect())
return false;
if (!filename.isEmpty()) {
const auto query = _queryManager.get(PreparedSqlQueryManager::GetFileRecordQuery, QByteArrayLiteral(GET_FILE_RECORD_QUERY " WHERE phash=?1"), _db);
if (!query) {
return false;
}
query->bindValue(1, getPHash(filename));
if (!query->exec()) {
close();
return false;
}
auto next = query->next();
if (!next.ok) {
QString err = query->error();
qCWarning(lcDb) << "No journal entry found for" << filename << "Error:" << err;
close();
return false;
}
if (next.hasData) {
fillFileRecordFromGetQuery(*rec, *query);
}
}
return true;
}
bool SyncJournalDb::getFileRecordByE2eMangledName(const QString &mangledName, SyncJournalFileRecord *rec)
{
QMutexLocker locker(&_mutex);
// Reset the output var in case the caller is reusing it.
Q_ASSERT(rec);
rec->_path.clear();
Q_ASSERT(!rec->isValid());
if (_metadataTableIsEmpty) {
return true; // no error, yet nothing found (rec->isValid() == false)
}
if (!checkConnect()) {
return false;
}
if (!mangledName.isEmpty()) {
const auto query = _queryManager.get(PreparedSqlQueryManager::GetFileRecordQueryByMangledName, QByteArrayLiteral(GET_FILE_RECORD_QUERY " WHERE e2eMangledName=?1"), _db);
if (!query) {
return false;
}
query->bindValue(1, mangledName);
if (!query->exec()) {
close();
return false;
}
auto next = query->next();
if (!next.ok) {
QString err = query->error();
qCWarning(lcDb) << "No journal entry found for mangled name" << mangledName << "Error: " << err;
close();
return false;
}
if (next.hasData) {
fillFileRecordFromGetQuery(*rec, *query);
}
}
return true;
}
bool SyncJournalDb::getFileRecordByInode(quint64 inode, SyncJournalFileRecord *rec)
{
QMutexLocker locker(&_mutex);
// Reset the output var in case the caller is reusing it.
Q_ASSERT(rec);
rec->_path.clear();
Q_ASSERT(!rec->isValid());
if (!inode || _metadataTableIsEmpty)
return true; // no error, yet nothing found (rec->isValid() == false)
if (!checkConnect())
return false;
const auto query = _queryManager.get(PreparedSqlQueryManager::GetFileRecordQueryByInode, QByteArrayLiteral(GET_FILE_RECORD_QUERY " WHERE inode=?1"), _db);
if (!query)
return false;
query->bindValue(1, inode);
if (!query->exec())
return false;
auto next = query->next();
if (!next.ok)
return false;
if (next.hasData)
fillFileRecordFromGetQuery(*rec, *query);
return true;
}
bool SyncJournalDb::getFileRecordsByFileId(const QByteArray &fileId, const std::function<void(const SyncJournalFileRecord &)> &rowCallback)
{
QMutexLocker locker(&_mutex);
if (fileId.isEmpty() || _metadataTableIsEmpty)
return true; // no error, yet nothing found (rec->isValid() == false)
if (!checkConnect())
return false;
const auto query = _queryManager.get(PreparedSqlQueryManager::GetFileRecordQueryByFileId, QByteArrayLiteral(GET_FILE_RECORD_QUERY " WHERE fileid=?1"), _db);
if (!query) {
return false;
}
query->bindValue(1, fileId);
if (!query->exec())
return false;
forever {
auto next = query->next();
if (!next.ok)
return false;
if (!next.hasData)
break;
SyncJournalFileRecord rec;
fillFileRecordFromGetQuery(rec, *query);
rowCallback(rec);
}
return true;
}
bool SyncJournalDb::getFilesBelowPath(const QByteArray &path, const std::function<void(const SyncJournalFileRecord&)> &rowCallback)
{
QMutexLocker locker(&_mutex);
if (_metadataTableIsEmpty)
return true; // no error, yet nothing found
if (!checkConnect())
return false;
auto _exec = [&rowCallback](SqlQuery &query) {
if (!query.exec()) {
return false;
}
forever {
auto next = query.next();
if (!next.ok)
return false;
if (!next.hasData)
break;
SyncJournalFileRecord rec;
fillFileRecordFromGetQuery(rec, query);
rowCallback(rec);
}
return true;
};
if(path.isEmpty()) {
// Since the path column doesn't store the starting /, the getFilesBelowPathQuery
// can't be used for the root path "". It would scan for (path > '/' and path < '0')
// and find nothing. So, unfortunately, we have to use a different query for
// retrieving the whole tree.
const auto query = _queryManager.get(PreparedSqlQueryManager::GetAllFilesQuery, QByteArrayLiteral(GET_FILE_RECORD_QUERY " ORDER BY path||'/' ASC"), _db);
if (!query) {
return false;
}
return _exec(*query);
} else {
// This query is used to skip discovery and fill the tree from the
// database instead
const auto query = _queryManager.get(PreparedSqlQueryManager::GetFilesBelowPathQuery, QByteArrayLiteral(GET_FILE_RECORD_QUERY " WHERE " IS_PREFIX_PATH_OF("?1", "path")
" OR " IS_PREFIX_PATH_OF("?1", "e2eMangledName")
// We want to ensure that the contents of a directory are sorted
// directly behind the directory itself. Without this ORDER BY
// an ordering like foo, foo-2, foo/file would be returned.
// With the trailing /, we get foo-2, foo, foo/file. This property
// is used in fill_tree_from_db().
" ORDER BY path||'/' ASC"),
_db);
if (!query) {
return false;
}
query->bindValue(1, path);
return _exec(*query);
}
}
bool SyncJournalDb::listFilesInPath(const QByteArray& path,
const std::function<void (const SyncJournalFileRecord &)>& rowCallback)
{
QMutexLocker locker(&_mutex);
if (_metadataTableIsEmpty)
return true;
if (!checkConnect())
return false;
const auto query = _queryManager.get(PreparedSqlQueryManager::ListFilesInPathQuery, QByteArrayLiteral(GET_FILE_RECORD_QUERY " WHERE parent_hash(path) = ?1 ORDER BY path||'/' ASC"), _db);
if (!query) {
return false;
}
query->bindValue(1, getPHash(path));
if (!query->exec())
return false;
forever {
auto next = query->next();
if (!next.ok)
return false;
if (!next.hasData)
break;
SyncJournalFileRecord rec;
fillFileRecordFromGetQuery(rec, *query);
if (!rec._path.startsWith(path) || rec._path.indexOf("/", path.size() + 1) > 0) {
qWarning(lcDb) << "hash collision" << path << rec.path();
continue;
}
rowCallback(rec);
}
return true;
}
int SyncJournalDb::getFileRecordCount()
{
QMutexLocker locker(&_mutex);
SqlQuery query(_db);
query.prepare("SELECT COUNT(*) FROM metadata");
if (!query.exec()) {
return -1;
}
if (query.next().hasData) {
int count = query.intValue(0);
return count;
}
return -1;
}
bool SyncJournalDb::updateFileRecordChecksum(const QString &filename,
const QByteArray &contentChecksum,
const QByteArray &contentChecksumType)
{
QMutexLocker locker(&_mutex);
qCInfo(lcDb) << "Updating file checksum" << filename << contentChecksum << contentChecksumType;
const qint64 phash = getPHash(filename.toUtf8());
if (!checkConnect()) {
qCWarning(lcDb) << "Failed to connect database.";
return false;
}
int checksumTypeId = mapChecksumType(contentChecksumType);
const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordChecksumQuery, QByteArrayLiteral("UPDATE metadata"
" SET contentChecksum = ?2, contentChecksumTypeId = ?3"
" WHERE phash == ?1;"),
_db);
if (!query) {
return false;
}
query->bindValue(1, phash);
query->bindValue(2, contentChecksum);
query->bindValue(3, checksumTypeId);
return query->exec();
}
bool SyncJournalDb::updateLocalMetadata(const QString &filename,
qint64 modtime, qint64 size, quint64 inode, const SyncJournalFileLockInfo &lockInfo)
{
QMutexLocker locker(&_mutex);
qCInfo(lcDb) << "Updating local metadata for:" << filename << modtime << size << inode;
const qint64 phash = getPHash(filename.toUtf8());
if (!checkConnect()) {
qCWarning(lcDb) << "Failed to connect database.";
return false;
}
const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordLocalMetadataQuery, QByteArrayLiteral("UPDATE metadata"
" SET inode=?2, modtime=?3, filesize=?4, lock=?5, lockType=?6,"
" lockOwnerDisplayName=?7, lockOwnerId=?8, lockOwnerEditor = ?9,"
" lockTime=?10, lockTimeout=?11"
" WHERE phash == ?1;"),
_db);
if (!query) {
return false;
}
query->bindValue(1, phash);
query->bindValue(2, inode);
query->bindValue(3, modtime);
query->bindValue(4, size);
query->bindValue(5, lockInfo._locked ? 1 : 0);
query->bindValue(6, lockInfo._lockOwnerType);
query->bindValue(7, lockInfo._lockOwnerDisplayName);
query->bindValue(8, lockInfo._lockOwnerId);
query->bindValue(9, lockInfo._lockEditorApp);
query->bindValue(10, lockInfo._lockTime);
query->bindValue(11, lockInfo._lockTimeout);
return query->exec();
}
Optional<SyncJournalDb::HasHydratedDehydrated> SyncJournalDb::hasHydratedOrDehydratedFiles(const QByteArray &filename)
{
QMutexLocker locker(&_mutex);
if (!checkConnect())
return {};
const auto query = _queryManager.get(PreparedSqlQueryManager::CountDehydratedFilesQuery, QByteArrayLiteral("SELECT DISTINCT type FROM metadata"
" WHERE (" IS_PREFIX_PATH_OR_EQUAL("?1", "path") " OR ?1 == '');"),
_db);
if (!query) {
return {};
}
query->bindValue(1, filename);
if (!query->exec())
return {};
HasHydratedDehydrated result;
forever {
auto next = query->next();
if (!next.ok)
return {};
if (!next.hasData)
break;
auto type = static_cast<ItemType>(query->intValue(0));
if (type == ItemTypeFile || type == ItemTypeVirtualFileDehydration)
result.hasHydrated = true;
if (type == ItemTypeVirtualFile || type == ItemTypeVirtualFileDownload)
result.hasDehydrated = true;
}
return result;
}
static void toDownloadInfo(SqlQuery &query, SyncJournalDb::DownloadInfo *res)
{
bool ok = true;
res->_tmpfile = query.stringValue(0);
res->_etag = query.baValue(1);
res->_errorCount = query.intValue(2);
res->_valid = ok;
}
static bool deleteBatch(SqlQuery &query, const QStringList &entries, const QString &name)
{
if (entries.isEmpty())
return true;
qCDebug(lcDb) << "Removing stale" << name << "entries:" << entries.join(QStringLiteral(", "));
// FIXME: Was ported from execBatch, check if correct!
foreach (const QString &entry, entries) {
query.reset_and_clear_bindings();
query.bindValue(1, entry);
if (!query.exec()) {
return false;
}
}
return true;
}
SyncJournalDb::DownloadInfo SyncJournalDb::getDownloadInfo(const QString &file)
{
QMutexLocker locker(&_mutex);
DownloadInfo res;
if (checkConnect()) {
const auto query = _queryManager.get(PreparedSqlQueryManager::GetDownloadInfoQuery, QByteArrayLiteral("SELECT tmpfile, etag, errorcount FROM downloadinfo WHERE path=?1"), _db);
if (!query) {
return res;
}
query->bindValue(1, file);
if (!query->exec()) {
return res;
}
if (query->next().hasData) {
toDownloadInfo(*query, &res);
}
}
return res;
}
void SyncJournalDb::setDownloadInfo(const QString &file, const SyncJournalDb::DownloadInfo &i)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return;
}
if (i._valid) {
const auto query = _queryManager.get(PreparedSqlQueryManager::SetDownloadInfoQuery, QByteArrayLiteral("INSERT OR REPLACE INTO downloadinfo "
"(path, tmpfile, etag, errorcount) "
"VALUES ( ?1 , ?2, ?3, ?4 )"),
_db);
if (!query) {
return;
}
query->bindValue(1, file);
query->bindValue(2, i._tmpfile);
query->bindValue(3, i._etag);
query->bindValue(4, i._errorCount);
query->exec();
} else {
const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteDownloadInfoQuery);
query->bindValue(1, file);
query->exec();
}
}
QVector<SyncJournalDb::DownloadInfo> SyncJournalDb::getAndDeleteStaleDownloadInfos(const QSet<QString> &keep)
{
QVector<SyncJournalDb::DownloadInfo> empty_result;
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return empty_result;
}
SqlQuery query(_db);
// The selected values *must* match the ones expected by toDownloadInfo().
query.prepare("SELECT tmpfile, etag, errorcount, path FROM downloadinfo");
if (!query.exec()) {
return empty_result;
}
QStringList superfluousPaths;
QVector<SyncJournalDb::DownloadInfo> deleted_entries;
while (query.next().hasData) {
const QString file = query.stringValue(3); // path
if (!keep.contains(file)) {
superfluousPaths.append(file);
DownloadInfo info;
toDownloadInfo(query, &info);
deleted_entries.append(info);
}
}
{
const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteDownloadInfoQuery);
if (!deleteBatch(*query, superfluousPaths, QStringLiteral("downloadinfo"))) {
return empty_result;
}
}
return deleted_entries;
}
int SyncJournalDb::downloadInfoCount()
{
int re = 0;
QMutexLocker locker(&_mutex);
if (checkConnect()) {
SqlQuery query("SELECT count(*) FROM downloadinfo", _db);
if (!query.exec()) {
sqlFail(QStringLiteral("Count number of downloadinfo entries failed"), query);
}
if (query.next().hasData) {
re = query.intValue(0);
}
}
return re;
}
SyncJournalDb::UploadInfo SyncJournalDb::getUploadInfo(const QString &file)
{
QMutexLocker locker(&_mutex);
UploadInfo res;
if (checkConnect()) {
const auto query = _queryManager.get(PreparedSqlQueryManager::GetUploadInfoQuery, QByteArrayLiteral("SELECT chunk, transferid, errorcount, size, modtime, contentChecksum FROM "
"uploadinfo WHERE path=?1"),
_db);
if (!query) {
return res;
}
query->bindValue(1, file);
if (!query->exec()) {
return res;
}
if (query->next().hasData) {
bool ok = true;
res._chunk = query->intValue(0);
res._transferid = query->int64Value(1);
res._errorCount = query->intValue(2);
res._size = query->int64Value(3);
res._modtime = query->int64Value(4);
res._contentChecksum = query->baValue(5);
res._valid = ok;
}
}
return res;
}
void SyncJournalDb::setUploadInfo(const QString &file, const SyncJournalDb::UploadInfo &i)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return;
}
if (i._valid) {
const auto query = _queryManager.get(PreparedSqlQueryManager::SetUploadInfoQuery, QByteArrayLiteral("INSERT OR REPLACE INTO uploadinfo "
"(path, chunk, transferid, errorcount, size, modtime, contentChecksum) "
"VALUES ( ?1 , ?2, ?3 , ?4 , ?5, ?6 , ?7 )"),
_db);
if (!query) {
return;
}
query->bindValue(1, file);
query->bindValue(2, i._chunk);
query->bindValue(3, i._transferid);
query->bindValue(4, i._errorCount);
query->bindValue(5, i._size);
query->bindValue(6, i._modtime);
query->bindValue(7, i._contentChecksum);
if (!query->exec()) {
return;
}
} else {
const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteUploadInfoQuery);
query->bindValue(1, file);
if (!query->exec()) {
return;
}
}
}
QVector<uint> SyncJournalDb::deleteStaleUploadInfos(const QSet<QString> &keep)
{
QMutexLocker locker(&_mutex);
QVector<uint> ids;
if (!checkConnect()) {
return ids;
}
SqlQuery query(_db);
query.prepare("SELECT path,transferid FROM uploadinfo");
if (!query.exec()) {
return ids;
}
QStringList superfluousPaths;
while (query.next().hasData) {
const QString file = query.stringValue(0);
if (!keep.contains(file)) {
superfluousPaths.append(file);
ids.append(query.intValue(1));
}
}
const auto deleteUploadInfoQuery = _queryManager.get(PreparedSqlQueryManager::DeleteUploadInfoQuery);
deleteBatch(*deleteUploadInfoQuery, superfluousPaths, QStringLiteral("uploadinfo"));
return ids;
}
SyncJournalErrorBlacklistRecord SyncJournalDb::errorBlacklistEntry(const QString &file)
{
QMutexLocker locker(&_mutex);
SyncJournalErrorBlacklistRecord entry;
if (file.isEmpty())
return entry;
if (checkConnect()) {
const auto query = _queryManager.get(PreparedSqlQueryManager::GetErrorBlacklistQuery);
query->bindValue(1, file);
if (query->exec()) {
if (query->next().hasData) {
entry._lastTryEtag = query->baValue(0);
entry._lastTryModtime = query->int64Value(1);
entry._retryCount = query->intValue(2);
entry._errorString = query->stringValue(3);
entry._lastTryTime = query->int64Value(4);
entry._ignoreDuration = query->int64Value(5);
entry._renameTarget = query->stringValue(6);
entry._errorCategory = static_cast<SyncJournalErrorBlacklistRecord::Category>(
query->intValue(7));
entry._requestId = query->baValue(8);
entry._file = file;
}
}
}
return entry;
}
bool SyncJournalDb::deleteStaleErrorBlacklistEntries(const QSet<QString> &keep)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return false;
}
SqlQuery query(_db);
query.prepare("SELECT path FROM blacklist");
if (!query.exec()) {
return false;
}
QStringList superfluousPaths;
while (query.next().hasData) {
const QString file = query.stringValue(0);
if (!keep.contains(file)) {
superfluousPaths.append(file);
}
}
SqlQuery delQuery(_db);
delQuery.prepare("DELETE FROM blacklist WHERE path = ?");
return deleteBatch(delQuery, superfluousPaths, QStringLiteral("blacklist"));
}
void SyncJournalDb::deleteStaleFlagsEntries()
{
QMutexLocker locker(&_mutex);
if (!checkConnect())
return;
SqlQuery delQuery("DELETE FROM flags WHERE path != '' AND path NOT IN (SELECT path from metadata);", _db);
if (!delQuery.exec()) {
sqlFail(QStringLiteral("deleteStaleFlagsEntries"), delQuery);
}
}
int SyncJournalDb::errorBlackListEntryCount()
{
int re = 0;
QMutexLocker locker(&_mutex);
if (checkConnect()) {
SqlQuery query("SELECT count(*) FROM blacklist", _db);
if (!query.exec()) {
sqlFail(QStringLiteral("Count number of blacklist entries failed"), query);
}
if (query.next().hasData) {
re = query.intValue(0);
}
}
return re;
}
int SyncJournalDb::wipeErrorBlacklist()
{
QMutexLocker locker(&_mutex);
if (checkConnect()) {
SqlQuery query(_db);
query.prepare("DELETE FROM blacklist");
if (!query.exec()) {
sqlFail(QStringLiteral("Deletion of whole blacklist failed"), query);
return -1;
}
return query.numRowsAffected();
}
return -1;
}
void SyncJournalDb::wipeErrorBlacklistEntry(const QString &file)
{
if (file.isEmpty()) {
return;
}
QMutexLocker locker(&_mutex);
if (checkConnect()) {
SqlQuery query(_db);
query.prepare("DELETE FROM blacklist WHERE path=?1");
query.bindValue(1, file);
if (!query.exec()) {
sqlFail(QStringLiteral("Deletion of blacklist item failed."), query);
}
}
}
void SyncJournalDb::wipeErrorBlacklistCategory(SyncJournalErrorBlacklistRecord::Category category)
{
QMutexLocker locker(&_mutex);
if (checkConnect()) {
SqlQuery query(_db);
query.prepare("DELETE FROM blacklist WHERE errorCategory=?1");
query.bindValue(1, category);
if (!query.exec()) {
sqlFail(QStringLiteral("Deletion of blacklist category failed."), query);
}
}
}
void SyncJournalDb::setErrorBlacklistEntry(const SyncJournalErrorBlacklistRecord &item)
{
QMutexLocker locker(&_mutex);
qCInfo(lcDb) << "Setting blacklist entry for" << item._file << item._retryCount
<< item._errorString << item._lastTryTime << item._ignoreDuration
<< item._lastTryModtime << item._lastTryEtag << item._renameTarget
<< item._errorCategory;
if (!checkConnect()) {
return;
}
const auto query = _queryManager.get(PreparedSqlQueryManager::SetErrorBlacklistQuery, QByteArrayLiteral("INSERT OR REPLACE INTO blacklist "
"(path, lastTryEtag, lastTryModtime, retrycount, errorstring, lastTryTime, ignoreDuration, renameTarget, errorCategory, requestId) "
"VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"),
_db);
if (!query) {
return;
}
query->bindValue(1, item._file);
query->bindValue(2, item._lastTryEtag);
query->bindValue(3, item._lastTryModtime);
query->bindValue(4, item._retryCount);
query->bindValue(5, item._errorString);
query->bindValue(6, item._lastTryTime);
query->bindValue(7, item._ignoreDuration);
query->bindValue(8, item._renameTarget);
query->bindValue(9, item._errorCategory);
query->bindValue(10, item._requestId);
query->exec();
}
QVector<SyncJournalDb::PollInfo> SyncJournalDb::getPollInfos()
{
QMutexLocker locker(&_mutex);
QVector<SyncJournalDb::PollInfo> res;
if (!checkConnect())
return res;
SqlQuery query("SELECT path, modtime, filesize, pollpath FROM async_poll", _db);
if (!query.exec()) {
return res;
}
while (query.next().hasData) {
PollInfo info;
info._file = query.stringValue(0);
info._modtime = query.int64Value(1);
info._fileSize = query.int64Value(2);
info._url = query.stringValue(3);
res.append(info);
}
return res;
}
void SyncJournalDb::setPollInfo(const SyncJournalDb::PollInfo &info)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return;
}
if (info._url.isEmpty()) {
qCDebug(lcDb) << "Deleting Poll job" << info._file;
SqlQuery query("DELETE FROM async_poll WHERE path=?", _db);
query.bindValue(1, info._file);
if (!query.exec()) {
sqlFail(QStringLiteral("setPollInfo DELETE FROM async_poll"), query);
}
} else {
SqlQuery query("INSERT OR REPLACE INTO async_poll (path, modtime, filesize, pollpath) VALUES( ? , ? , ? , ? )", _db);
query.bindValue(1, info._file);
query.bindValue(2, info._modtime);
query.bindValue(3, info._fileSize);
query.bindValue(4, info._url);
if (!query.exec()) {
sqlFail(QStringLiteral("setPollInfo INSERT OR REPLACE INTO async_poll"), query);
}
}
}
QStringList SyncJournalDb::getSelectiveSyncList(SyncJournalDb::SelectiveSyncListType type, bool *ok)
{
QStringList result;
ASSERT(ok);
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
*ok = false;
return result;
}
const auto query = _queryManager.get(PreparedSqlQueryManager::GetSelectiveSyncListQuery, QByteArrayLiteral("SELECT path FROM selectivesync WHERE type=?1"), _db);
if (!query) {
*ok = false;
return result;
}
query->bindValue(1, int(type));
if (!query->exec()) {
*ok = false;
return result;
}
forever {
auto next = query->next();
if (!next.ok) {
*ok = false;
return result;
}
if (!next.hasData)
break;
auto entry = query->stringValue(0);
if (!entry.endsWith(QLatin1Char('/'))) {
entry.append(QLatin1Char('/'));
}
result.append(entry);
}
*ok = true;
return result;
}
void SyncJournalDb::setSelectiveSyncList(SyncJournalDb::SelectiveSyncListType type, const QStringList &list)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return;
}
startTransaction();
//first, delete all entries of this type
SqlQuery delQuery("DELETE FROM selectivesync WHERE type == ?1", _db);
delQuery.bindValue(1, int(type));
if (!delQuery.exec()) {
qCWarning(lcDb) << "SQL error when deleting selective sync list" << list << delQuery.error();
}
SqlQuery insQuery("INSERT INTO selectivesync VALUES (?1, ?2)", _db);
foreach (const auto &path, list) {
insQuery.reset_and_clear_bindings();
insQuery.bindValue(1, path);
insQuery.bindValue(2, int(type));
if (!insQuery.exec()) {
qCWarning(lcDb) << "SQL error when inserting into selective sync" << type << path << delQuery.error();
}
}
commitInternal(QStringLiteral("setSelectiveSyncList"));
}
void SyncJournalDb::avoidRenamesOnNextSync(const QByteArray &path)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return;
}
SqlQuery query(_db);
query.prepare("UPDATE metadata SET fileid = '', inode = '0' WHERE " IS_PREFIX_PATH_OR_EQUAL("?1", "path"));
query.bindValue(1, path);
if (!query.exec()) {
sqlFail(QStringLiteral("avoidRenamesOnNextSync path: %1").arg(QString::fromUtf8(path)), query);
}
// We also need to remove the ETags so the update phase refreshes the directory paths
// on the next sync
schedulePathForRemoteDiscovery(path);
}
void SyncJournalDb::schedulePathForRemoteDiscovery(const QByteArray &fileName)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return;
}
// Remove trailing slash
auto argument = fileName;
if (argument.endsWith('/'))
argument.chop(1);
SqlQuery query(_db);
// This query will match entries for which the path is a prefix of fileName
// Note: CSYNC_FTW_TYPE_DIR == 2
query.prepare("UPDATE metadata SET md5='_invalid_' WHERE " IS_PREFIX_PATH_OR_EQUAL("path", "?1") " AND type == 2;");
query.bindValue(1, argument);
if (!query.exec()) {
sqlFail(QStringLiteral("schedulePathForRemoteDiscovery path: %1").arg(QString::fromUtf8(fileName)), query);
}
// Prevent future overwrite of the etags of this folder and all
// parent folders for this sync
argument.append('/');
_etagStorageFilter.append(argument);
}
void SyncJournalDb::clearEtagStorageFilter()
{
_etagStorageFilter.clear();
}
void SyncJournalDb::forceRemoteDiscoveryNextSync()
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return;
}
forceRemoteDiscoveryNextSyncLocked();
}
void SyncJournalDb::forceRemoteDiscoveryNextSyncLocked()
{
qCInfo(lcDb) << "Forcing remote re-discovery by deleting folder Etags";
SqlQuery deleteRemoteFolderEtagsQuery(_db);
deleteRemoteFolderEtagsQuery.prepare("UPDATE metadata SET md5='_invalid_' WHERE type=2;");
if (!deleteRemoteFolderEtagsQuery.exec()) {
sqlFail(QStringLiteral("forceRemoteDiscoveryNextSyncLocked"), deleteRemoteFolderEtagsQuery);
}
}
QByteArray SyncJournalDb::getChecksumType(int checksumTypeId)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return QByteArray();
}
// Retrieve the id
const auto query = _queryManager.get(PreparedSqlQueryManager::GetChecksumTypeQuery, QByteArrayLiteral("SELECT name FROM checksumtype WHERE id=?1"), _db);
if (!query) {
return {};
}
query->bindValue(1, checksumTypeId);
if (!query->exec()) {
return QByteArray();
}
if (!query->next().hasData) {
qCWarning(lcDb) << "No checksum type mapping found for" << checksumTypeId;
return QByteArray();
}
return query->baValue(0);
}
int SyncJournalDb::mapChecksumType(const QByteArray &checksumType)
{
if (checksumType.isEmpty()) {
return 0;
}
auto it = _checksymTypeCache.find(checksumType);
if (it != _checksymTypeCache.end())
return *it;
// Ensure the checksum type is in the db
{
const auto query = _queryManager.get(PreparedSqlQueryManager::InsertChecksumTypeQuery, QByteArrayLiteral("INSERT OR IGNORE INTO checksumtype (name) VALUES (?1)"), _db);
if (!query) {
return 0;
}
query->bindValue(1, checksumType);
if (!query->exec()) {
return 0;
}
}
// Retrieve the id
{
const auto query = _queryManager.get(PreparedSqlQueryManager::GetChecksumTypeIdQuery, QByteArrayLiteral("SELECT id FROM checksumtype WHERE name=?1"), _db);
if (!query) {
return 0;
}
query->bindValue(1, checksumType);
if (!query->exec()) {
return 0;
}
if (!query->next().hasData) {
qCWarning(lcDb) << "No checksum type mapping found for" << checksumType;
return 0;
}
auto value = query->intValue(0);
_checksymTypeCache[checksumType] = value;
return value;
}
}
QByteArray SyncJournalDb::dataFingerprint()
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return QByteArray();
}
const auto query = _queryManager.get(PreparedSqlQueryManager::GetDataFingerprintQuery, QByteArrayLiteral("SELECT fingerprint FROM datafingerprint"), _db);
if (!query) {
return QByteArray();
}
if (!query->exec()) {
return QByteArray();
}
if (!query->next().hasData) {
return QByteArray();
}
return query->baValue(0);
}
void SyncJournalDb::setDataFingerprint(const QByteArray &dataFingerprint)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return;
}
const auto setDataFingerprintQuery1 = _queryManager.get(PreparedSqlQueryManager::SetDataFingerprintQuery1, QByteArrayLiteral("DELETE FROM datafingerprint;"), _db);
const auto setDataFingerprintQuery2 = _queryManager.get(PreparedSqlQueryManager::SetDataFingerprintQuery2, QByteArrayLiteral("INSERT INTO datafingerprint (fingerprint) VALUES (?1);"), _db);
if (!setDataFingerprintQuery1 || !setDataFingerprintQuery2) {
return;
}
setDataFingerprintQuery1->exec();
setDataFingerprintQuery2->bindValue(1, dataFingerprint);
setDataFingerprintQuery2->exec();
}
void SyncJournalDb::setConflictRecord(const ConflictRecord &record)
{
QMutexLocker locker(&_mutex);
if (!checkConnect())
return;
const auto query = _queryManager.get(PreparedSqlQueryManager::SetConflictRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO conflicts "
"(path, baseFileId, baseModtime, baseEtag, basePath) "
"VALUES (?1, ?2, ?3, ?4, ?5);"),
_db);
ASSERT(query)
query->bindValue(1, record.path);
query->bindValue(2, record.baseFileId);
query->bindValue(3, record.baseModtime);
query->bindValue(4, record.baseEtag);
query->bindValue(5, record.initialBasePath);
ASSERT(query->exec())
}
ConflictRecord SyncJournalDb::conflictRecord(const QByteArray &path)
{
ConflictRecord entry;
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return entry;
}
const auto query = _queryManager.get(PreparedSqlQueryManager::GetConflictRecordQuery, QByteArrayLiteral("SELECT baseFileId, baseModtime, baseEtag, basePath FROM conflicts WHERE path=?1;"), _db);
ASSERT(query)
query->bindValue(1, path);
ASSERT(query->exec())
if (!query->next().hasData)
return entry;
entry.path = path;
entry.baseFileId = query->baValue(0);
entry.baseModtime = query->int64Value(1);
entry.baseEtag = query->baValue(2);
entry.initialBasePath = query->baValue(3);
return entry;
}
void SyncJournalDb::deleteConflictRecord(const QByteArray &path)
{
QMutexLocker locker(&_mutex);
if (!checkConnect())
return;
const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteConflictRecordQuery, QByteArrayLiteral("DELETE FROM conflicts WHERE path=?1;"), _db);
ASSERT(query)
query->bindValue(1, path);
ASSERT(query->exec())
}
QByteArrayList SyncJournalDb::conflictRecordPaths()
{
QMutexLocker locker(&_mutex);
if (!checkConnect())
return {};
SqlQuery query(_db);
query.prepare("SELECT path FROM conflicts");
ASSERT(query.exec());
QByteArrayList paths;
while (query.next().hasData)
paths.append(query.baValue(0));
return paths;
}
QByteArray SyncJournalDb::conflictFileBaseName(const QByteArray &conflictName)
{
auto conflict = conflictRecord(conflictName);
QByteArray result;
if (conflict.isValid()) {
if (!getFileRecordsByFileId(conflict.baseFileId, [&result](const SyncJournalFileRecord &record) {
if (!record._path.isEmpty())
result = record._path;
})) {
qCWarning(lcDb) << "conflictFileBaseName failed to getFileRecordsByFileId: " << conflictName;
}
}
if (result.isEmpty()) {
result = Utility::conflictFileBaseNameFromPattern(conflictName);
}
return result;
}
void SyncJournalDb::clearFileTable()
{
QMutexLocker lock(&_mutex);
SqlQuery query(_db);
query.prepare("DELETE FROM metadata;");
if (!query.exec()) {
sqlFail(QStringLiteral("clearFileTable"), query);
}
}
void SyncJournalDb::markVirtualFileForDownloadRecursively(const QByteArray &path)
{
QMutexLocker lock(&_mutex);
if (!checkConnect())
return;
static_assert(ItemTypeVirtualFile == 4 && ItemTypeVirtualFileDownload == 5, "");
SqlQuery query("UPDATE metadata SET type=5 WHERE "
"(" IS_PREFIX_PATH_OF("?1", "path") " OR ?1 == '') "
"AND type=4;", _db);
query.bindValue(1, path);
if (!query.exec()) {
sqlFail(QStringLiteral("markVirtualFileForDownloadRecursively UPDATE metadata SET type=5 path: %1").arg(QString::fromUtf8(path)), query);
}
// We also must make sure we do not read the files from the database (same logic as in schedulePathForRemoteDiscovery)
// This includes all the parents up to the root, but also all the directory within the selected dir.
static_assert(ItemTypeDirectory == 2, "");
query.prepare("UPDATE metadata SET md5='_invalid_' WHERE "
"(" IS_PREFIX_PATH_OF("?1", "path") " OR ?1 == '' OR " IS_PREFIX_PATH_OR_EQUAL("path", "?1") ") AND type == 2;");
query.bindValue(1, path);
if (!query.exec()) {
sqlFail(QStringLiteral("markVirtualFileForDownloadRecursively UPDATE metadata SET md5='_invalid_' path: %1").arg(QString::fromUtf8(path)), query);
}
}
Optional<PinState> SyncJournalDb::PinStateInterface::rawForPath(const QByteArray &path)
{
QMutexLocker lock(&_db->_mutex);
if (!_db->checkConnect())
return {};
const auto query = _db->_queryManager.get(PreparedSqlQueryManager::GetRawPinStateQuery, QByteArrayLiteral("SELECT pinState FROM flags WHERE path == ?1;"), _db->_db);
ASSERT(query)
query->bindValue(1, path);
query->exec();
auto next = query->next();
if (!next.ok)
return {};
// no-entry means Inherited
if (!next.hasData)
return PinState::Inherited;
return static_cast<PinState>(query->intValue(0));
}
Optional<PinState> SyncJournalDb::PinStateInterface::effectiveForPath(const QByteArray &path)
{
QMutexLocker lock(&_db->_mutex);
if (!_db->checkConnect())
return {};
const auto query = _db->_queryManager.get(PreparedSqlQueryManager::GetEffectivePinStateQuery, QByteArrayLiteral("SELECT pinState FROM flags WHERE"
// explicitly allow "" to represent the root path
// (it'd be great if paths started with a / and "/" could be the root)
" (" IS_PREFIX_PATH_OR_EQUAL("path", "?1") " OR path == '')"
" AND pinState is not null AND pinState != 0"
" ORDER BY length(path) DESC LIMIT 1;"),
_db->_db);
ASSERT(query)
query->bindValue(1, path);
query->exec();
auto next = query->next();
if (!next.ok)
return {};
// If the root path has no setting, assume AlwaysLocal
if (!next.hasData)
return PinState::AlwaysLocal;
return static_cast<PinState>(query->intValue(0));
}
Optional<PinState> SyncJournalDb::PinStateInterface::effectiveForPathRecursive(const QByteArray &path)
{
// Get the item's effective pin state. We'll compare subitem's pin states
// against this.
const auto basePin = effectiveForPath(path);
if (!basePin)
return {};
QMutexLocker lock(&_db->_mutex);
if (!_db->checkConnect())
return {};
// Find all the non-inherited pin states below the item
const auto query = _db->_queryManager.get(PreparedSqlQueryManager::GetSubPinsQuery, QByteArrayLiteral("SELECT DISTINCT pinState FROM flags WHERE"
" (" IS_PREFIX_PATH_OF("?1", "path") " OR ?1 == '')"
" AND pinState is not null and pinState != 0;"),
_db->_db);
ASSERT(query)
query->bindValue(1, path);
query->exec();
// Check if they are all identical
forever {
auto next = query->next();
if (!next.ok)
return {};
if (!next.hasData)
break;
const auto subPin = static_cast<PinState>(query->intValue(0));
if (subPin != *basePin)
return PinState::Inherited;
}
return *basePin;
}
void SyncJournalDb::PinStateInterface::setForPath(const QByteArray &path, PinState state)
{
QMutexLocker lock(&_db->_mutex);
if (!_db->checkConnect())
return;
const auto query = _db->_queryManager.get(PreparedSqlQueryManager::SetPinStateQuery, QByteArrayLiteral(
// If we had sqlite >=3.24.0 everywhere this could be an upsert,
// making further flags columns easy
//"INSERT INTO flags(path, pinState) VALUES(?1, ?2)"
//" ON CONFLICT(path) DO UPDATE SET pinState=?2;"),
// Simple version that doesn't work nicely with multiple columns:
"INSERT OR REPLACE INTO flags(path, pinState) VALUES(?1, ?2);"),
_db->_db);
ASSERT(query)
query->bindValue(1, path);
query->bindValue(2, state);
query->exec();
}
void SyncJournalDb::PinStateInterface::wipeForPathAndBelow(const QByteArray &path)
{
QMutexLocker lock(&_db->_mutex);
if (!_db->checkConnect())
return;
const auto query = _db->_queryManager.get(PreparedSqlQueryManager::WipePinStateQuery, QByteArrayLiteral("DELETE FROM flags WHERE "
// Allow "" to delete everything
" (" IS_PREFIX_PATH_OR_EQUAL("?1", "path") " OR ?1 == '');"),
_db->_db);
ASSERT(query)
query->bindValue(1, path);
query->exec();
}
Optional<QVector<QPair<QByteArray, PinState>>>
SyncJournalDb::PinStateInterface::rawList()
{
QMutexLocker lock(&_db->_mutex);
if (!_db->checkConnect())
return {};
SqlQuery query("SELECT path, pinState FROM flags;", _db->_db);
if (!query.exec()) {
qCWarning(lcDb) << "SQL Error" << "PinStateInterface::rawList" << query.error();
_db->close();
ASSERT(false);
}
QVector<QPair<QByteArray, PinState>> result;
forever {
auto next = query.next();
if (!next.ok)
return {};
if (!next.hasData)
break;
result.append({ query.baValue(0), static_cast<PinState>(query.intValue(1)) });
}
return result;
}
SyncJournalDb::PinStateInterface SyncJournalDb::internalPinStates()
{
return {this};
}
void SyncJournalDb::commit(const QString &context, bool startTrans)
{
QMutexLocker lock(&_mutex);
commitInternal(context, startTrans);
}
void SyncJournalDb::commitIfNeededAndStartNewTransaction(const QString &context)
{
QMutexLocker lock(&_mutex);
if (_transaction == 1) {
commitInternal(context, true);
} else {
startTransaction();
}
}
bool SyncJournalDb::open()
{
QMutexLocker lock(&_mutex);
return checkConnect();
}
bool SyncJournalDb::isOpen()
{
QMutexLocker lock(&_mutex);
return _db.isOpen();
}
void SyncJournalDb::commitInternal(const QString &context, bool startTrans)
{
qCDebug(lcDb) << "Transaction commit" << context << (startTrans ? "and starting new transaction" : "");
commitTransaction();
if (startTrans) {
startTransaction();
}
}
SyncJournalDb::~SyncJournalDb()
{
if (isOpen()) {
close();
}
}
bool operator==(const SyncJournalDb::DownloadInfo &lhs,
const SyncJournalDb::DownloadInfo &rhs)
{
return lhs._errorCount == rhs._errorCount
&& lhs._etag == rhs._etag
&& lhs._tmpfile == rhs._tmpfile
&& lhs._valid == rhs._valid;
}
bool operator==(const SyncJournalDb::UploadInfo &lhs,
const SyncJournalDb::UploadInfo &rhs)
{
return lhs._errorCount == rhs._errorCount
&& lhs._chunk == rhs._chunk
&& lhs._modtime == rhs._modtime
&& lhs._valid == rhs._valid
&& lhs._size == rhs._size
&& lhs._transferid == rhs._transferid
&& lhs._contentChecksum == rhs._contentChecksum;
}
} // namespace OCC