Compare the hash of files with identical mtime/size #5589

* For conflicts where mtime and size are identical:

  a) If there's no remote checksum, skip (unchanged)
  b) If there's a remote checksum that's a useful hash, create a
     PropagateDownload job and compute the local hash. If the hashes
     are identical, don't download the file and just update metadata.

* Avoid exposing the existence of checksumTypeId beyond the database
  layer. This makes handling checksums easier in general because they
  can usually be treated as a single blob.

  This change was prompted by the difficulty of producing file_stat_t
  entries uniformly from PROPFINDs and the database.
This commit is contained in:
Christian Kamm 2017-06-14 12:14:46 +02:00
parent d50d8b86cf
commit 8160963110
25 changed files with 345 additions and 128 deletions

View file

@ -376,8 +376,7 @@ static int _csync_treewalk_visitor(void *obj, void *data) {
trav.error_status = cur->error_status; trav.error_status = cur->error_status;
trav.has_ignored_files = cur->has_ignored_files; trav.has_ignored_files = cur->has_ignored_files;
trav.checksum = cur->checksum; trav.checksumHeader = cur->checksumHeader;
trav.checksumTypeId = cur->checksumTypeId;
if( other_node ) { if( other_node ) {
csync_file_stat_t *other_stat = (csync_file_stat_t*)other_node->data; csync_file_stat_t *other_stat = (csync_file_stat_t*)other_node->data;
@ -670,7 +669,7 @@ void csync_file_stat_free(csync_file_stat_t *st)
SAFE_FREE(st->directDownloadCookies); SAFE_FREE(st->directDownloadCookies);
SAFE_FREE(st->etag); SAFE_FREE(st->etag);
SAFE_FREE(st->destpath); SAFE_FREE(st->destpath);
SAFE_FREE(st->checksum); SAFE_FREE(st->checksumHeader);
SAFE_FREE(st); SAFE_FREE(st);
} }
} }

View file

@ -223,6 +223,10 @@ struct csync_vio_file_stat_s {
enum csync_vio_file_flags_e flags; enum csync_vio_file_flags_e flags;
char *original_name; // only set if locale conversion fails char *original_name; // only set if locale conversion fails
// For remote file stats: the highest quality checksum the server provided
// in the "SHA1:324315da2143" form.
char *checksumHeader;
}; };
csync_vio_file_stat_t OCSYNC_EXPORT *csync_vio_file_stat_new(void); csync_vio_file_stat_t OCSYNC_EXPORT *csync_vio_file_stat_new(void);
@ -262,8 +266,7 @@ struct csync_tree_walk_file_s {
char *directDownloadUrl; char *directDownloadUrl;
char *directDownloadCookies; char *directDownloadCookies;
const char *checksum; const char *checksumHeader;
uint32_t checksumTypeId;
struct { struct {
int64_t size; int64_t size;
@ -304,8 +307,8 @@ typedef int (*csync_vio_stat_hook) (csync_vio_handle_t *dhhandle,
void *userdata); void *userdata);
/* Compute the checksum of the given \a checksumTypeId for \a path. */ /* Compute the checksum of the given \a checksumTypeId for \a path. */
typedef const char* (*csync_checksum_hook) ( typedef const char *(*csync_checksum_hook)(
const char *path, uint32_t checksumTypeId, void *userdata); const char *path, const char *otherChecksumHeader, void *userdata);
/** /**
* @brief Allocate a csync context. * @brief Allocate a csync context.

View file

@ -191,8 +191,11 @@ struct csync_file_stat_s {
char *directDownloadCookies; char *directDownloadCookies;
char remotePerm[REMOTE_PERM_BUF_SIZE+1]; char remotePerm[REMOTE_PERM_BUF_SIZE+1];
const char *checksum; // In the local tree, this can hold a checksum and its type if it is
uint32_t checksumTypeId; // computed during discovery for some reason.
// In the remote tree, this will have the server checksum, if available.
// In both cases, the format is "SHA1:baff".
const char *checksumHeader;
CSYNC_STATUS error_status; CSYNC_STATUS error_status;

View file

@ -65,6 +65,19 @@ static c_rbnode_t *_csync_check_ignored(c_rbtree_t *tree, const char *path, int
} }
} }
/* Returns true if we're reasonably certain that hash equality
* for the header means content equality.
*
* Cryptographic safety is not required - this is mainly
* intended to rule out checksums like Adler32 that are not intended for
* hashing and have high likelihood of collision with particular inputs.
*/
static bool _csync_is_collision_safe_hash(const char *checksum_header)
{
return strncmp(checksum_header, "SHA1:", 5) == 0
|| strncmp(checksum_header, "MD5:", 4) == 0;
}
/** /**
* The main function in the reconcile pass. * The main function in the reconcile pass.
* *
@ -272,11 +285,31 @@ static int _csync_merge_algorithm_visitor(void *obj, void *data) {
// Folders of the same path are always considered equals // Folders of the same path are always considered equals
is_conflict = false; is_conflict = false;
} else { } else {
// If the size or mtime is different, it's definitely a conflict.
is_conflict = ((other->size != cur->size) || (other->modtime != cur->modtime)); is_conflict = ((other->size != cur->size) || (other->modtime != cur->modtime));
// FIXME: do a binary comparision of the file here because of the following
// edge case: // It could be a conflict even if size and mtime match!
// The files could still have different content, even though the mtime //
// and size are the same. // In older client versions we always treated these cases as a
// non-conflict. This behavior is preserved in case the server
// doesn't provide a suitable content hash.
//
// When it does have one, however, we do create a job, but the job
// will compare hashes and avoid the download if they are equal.
const char *remoteChecksumHeader =
(ctx->current == REMOTE_REPLICA ? cur->checksumHeader : other->checksumHeader);
if (remoteChecksumHeader) {
is_conflict |= _csync_is_collision_safe_hash(remoteChecksumHeader);
}
// SO: If there is no checksum, we can have !is_conflict here
// even though the files have different content! This is an
// intentional tradeoff. Downloading and comparing files would
// be technically correct in this situation but leads to too
// much waste.
// In particular this kind of NEW/NEW situation with identical
// sizes and mtimes pops up when the local database is lost for
// whatever reason.
} }
if (ctx->current == REMOTE_REPLICA) { if (ctx->current == REMOTE_REPLICA) {
// If the files are considered equal, only update the DB with the etag from remote // If the files are considered equal, only update the DB with the etag from remote

View file

@ -225,7 +225,12 @@ int csync_statedb_close(CSYNC *ctx) {
return rc; return rc;
} }
#define METADATA_COLUMNS "phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, contentChecksum, contentChecksumTypeId" #define METADATA_QUERY \
"phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, " \
"filesize, ignoredChildrenRemote, " \
"contentchecksumtype.name || ':' || contentChecksum " \
"FROM metadata " \
"LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
// This funciton parses a line from the metadata table into the given csync_file_stat // This funciton parses a line from the metadata table into the given csync_file_stat
// structure which it is also allocating. // structure which it is also allocating.
@ -287,9 +292,8 @@ static int _csync_file_stat_from_metadata_table( csync_file_stat_t **st, sqlite3
if(column_count > 13) { if(column_count > 13) {
(*st)->has_ignored_files = sqlite3_column_int(stmt, 13); (*st)->has_ignored_files = sqlite3_column_int(stmt, 13);
} }
if(column_count > 15 && sqlite3_column_int(stmt, 15)) { if (column_count > 14 && sqlite3_column_text(stmt, 14)) {
(*st)->checksum = c_strdup( (char*) sqlite3_column_text(stmt, 14)); (*st)->checksumHeader = c_strdup((char *)sqlite3_column_text(stmt, 14));
(*st)->checksumTypeId = sqlite3_column_int(stmt, 15);
} }
} }
@ -313,7 +317,7 @@ csync_file_stat_t *csync_statedb_get_stat_by_hash(CSYNC *ctx,
} }
if( ctx->statedb.by_hash_stmt == NULL ) { if( ctx->statedb.by_hash_stmt == NULL ) {
const char *hash_query = "SELECT " METADATA_COLUMNS " FROM metadata WHERE phash=?1"; const char *hash_query = "SELECT " METADATA_QUERY " WHERE phash=?1";
SQLITE_BUSY_HANDLED(sqlite3_prepare_v2(ctx->statedb.db, hash_query, strlen(hash_query), &ctx->statedb.by_hash_stmt, NULL)); SQLITE_BUSY_HANDLED(sqlite3_prepare_v2(ctx->statedb.db, hash_query, strlen(hash_query), &ctx->statedb.by_hash_stmt, NULL));
ctx->statedb.lastReturnValue = rc; ctx->statedb.lastReturnValue = rc;
@ -356,7 +360,7 @@ csync_file_stat_t *csync_statedb_get_stat_by_file_id(CSYNC *ctx,
} }
if( ctx->statedb.by_fileid_stmt == NULL ) { if( ctx->statedb.by_fileid_stmt == NULL ) {
const char *query = "SELECT " METADATA_COLUMNS " FROM metadata WHERE fileid=?1"; const char *query = "SELECT " METADATA_QUERY " WHERE fileid=?1";
SQLITE_BUSY_HANDLED(sqlite3_prepare_v2(ctx->statedb.db, query, strlen(query), &ctx->statedb.by_fileid_stmt, NULL)); SQLITE_BUSY_HANDLED(sqlite3_prepare_v2(ctx->statedb.db, query, strlen(query), &ctx->statedb.by_fileid_stmt, NULL));
ctx->statedb.lastReturnValue = rc; ctx->statedb.lastReturnValue = rc;
@ -396,7 +400,7 @@ csync_file_stat_t *csync_statedb_get_stat_by_inode(CSYNC *ctx,
} }
if( ctx->statedb.by_inode_stmt == NULL ) { if( ctx->statedb.by_inode_stmt == NULL ) {
const char *inode_query = "SELECT " METADATA_COLUMNS " FROM metadata WHERE inode=?1"; const char *inode_query = "SELECT " METADATA_QUERY " WHERE inode=?1";
SQLITE_BUSY_HANDLED(sqlite3_prepare_v2(ctx->statedb.db, inode_query, strlen(inode_query), &ctx->statedb.by_inode_stmt, NULL)); SQLITE_BUSY_HANDLED(sqlite3_prepare_v2(ctx->statedb.db, inode_query, strlen(inode_query), &ctx->statedb.by_inode_stmt, NULL));
ctx->statedb.lastReturnValue = rc; ctx->statedb.lastReturnValue = rc;
@ -439,7 +443,7 @@ int csync_statedb_get_below_path( CSYNC *ctx, const char *path ) {
* In other words, anything that is between path+'/' and path+'0', * In other words, anything that is between path+'/' and path+'0',
* (because '0' follows '/' in ascii) * (because '0' follows '/' in ascii)
*/ */
const char *below_path_query = "SELECT " METADATA_COLUMNS " FROM metadata WHERE path > (?||'/') AND path < (?||'0') ORDER BY path||'/' ASC"; const char *below_path_query = "SELECT " METADATA_QUERY " WHERE path > (?||'/') AND path < (?||'0') ORDER BY path||'/' ASC";
SQLITE_BUSY_HANDLED(sqlite3_prepare_v2(ctx->statedb.db, below_path_query, -1, &stmt, NULL)); SQLITE_BUSY_HANDLED(sqlite3_prepare_v2(ctx->statedb.db, below_path_query, -1, &stmt, NULL));
ctx->statedb.lastReturnValue = rc; ctx->statedb.lastReturnValue = rc;
if( rc != SQLITE_OK ) { if( rc != SQLITE_OK ) {

View file

@ -287,16 +287,15 @@ static int _csync_detect_update(CSYNC *ctx, const char *file,
// Checksum comparison at this stage is only enabled for .eml files, // Checksum comparison at this stage is only enabled for .eml files,
// check #4754 #4755 // check #4754 #4755
bool isEmlFile = csync_fnmatch("*.eml", file, FNM_CASEFOLD) == 0; bool isEmlFile = csync_fnmatch("*.eml", file, FNM_CASEFOLD) == 0;
if (isEmlFile && fs->size == tmp->size && tmp->checksumTypeId) { if (isEmlFile && fs->size == tmp->size && tmp->checksumHeader) {
if (ctx->callbacks.checksum_hook) { if (ctx->callbacks.checksum_hook) {
st->checksum = ctx->callbacks.checksum_hook( st->checksumHeader = ctx->callbacks.checksum_hook(
file, tmp->checksumTypeId, file, tmp->checksumHeader,
ctx->callbacks.checksum_userdata); ctx->callbacks.checksum_userdata);
} }
bool checksumIdentical = false; bool checksumIdentical = false;
if (st->checksum) { if (st->checksumHeader) {
st->checksumTypeId = tmp->checksumTypeId; checksumIdentical = strncmp(st->checksumHeader, tmp->checksumHeader, 1000) == 0;
checksumIdentical = strncmp(st->checksum, tmp->checksum, 1000) == 0;
} }
if (checksumIdentical) { if (checksumIdentical) {
CSYNC_LOG(CSYNC_LOG_PRIORITY_TRACE, "NOTE: Checksums are identical, file did not actually change: %s", path); CSYNC_LOG(CSYNC_LOG_PRIORITY_TRACE, "NOTE: Checksums are identical, file did not actually change: %s", path);
@ -381,15 +380,14 @@ static int _csync_detect_update(CSYNC *ctx, const char *file,
// Verify the checksum where possible // Verify the checksum where possible
if (isRename && tmp->checksumTypeId && ctx->callbacks.checksum_hook if (isRename && tmp->checksumHeader && ctx->callbacks.checksum_hook
&& fs->type == CSYNC_VIO_FILE_TYPE_REGULAR) { && fs->type == CSYNC_VIO_FILE_TYPE_REGULAR) {
st->checksum = ctx->callbacks.checksum_hook( st->checksumHeader = ctx->callbacks.checksum_hook(
file, tmp->checksumTypeId, file, tmp->checksumHeader,
ctx->callbacks.checksum_userdata); ctx->callbacks.checksum_userdata);
if (st->checksum) { if (st->checksumHeader) {
CSYNC_LOG(CSYNC_LOG_PRIORITY_TRACE, "checking checksum of potential rename %s %s <-> %s", path, st->checksum, tmp->checksum); CSYNC_LOG(CSYNC_LOG_PRIORITY_TRACE, "checking checksum of potential rename %s %s <-> %s", path, st->checksumHeader, tmp->checksumHeader);
st->checksumTypeId = tmp->checksumTypeId; isRename = strncmp(st->checksumHeader, tmp->checksumHeader, 1000) == 0;
isRename = strncmp(st->checksum, tmp->checksum, 1000) == 0;
} }
} }
@ -506,6 +504,11 @@ out:
strncpy(st->remotePerm, fs->remotePerm, REMOTE_PERM_BUF_SIZE); strncpy(st->remotePerm, fs->remotePerm, REMOTE_PERM_BUF_SIZE);
} }
// For the remote: propagate the discovered checksum
if (fs->checksumHeader && ctx->current == REMOTE_REPLICA) {
st->checksumHeader = c_strdup(fs->checksumHeader);
}
st->phash = h; st->phash = h;
st->pathlen = len; st->pathlen = len;
memcpy(st->path, (len ? path : ""), len + 1); memcpy(st->path, (len ? path : ""), len + 1);

View file

@ -40,6 +40,9 @@ csync_vio_file_stat_t* csync_vio_file_stat_copy(csync_vio_file_stat_t *file_stat
if (file_stat_cpy->directDownloadUrl) { if (file_stat_cpy->directDownloadUrl) {
file_stat_cpy->directDownloadUrl = c_strdup(file_stat_cpy->directDownloadUrl); file_stat_cpy->directDownloadUrl = c_strdup(file_stat_cpy->directDownloadUrl);
} }
if (file_stat_cpy->checksumHeader) {
file_stat_cpy->checksumHeader = c_strdup(file_stat_cpy->checksumHeader);
}
file_stat_cpy->name = c_strdup(file_stat_cpy->name); file_stat_cpy->name = c_strdup(file_stat_cpy->name);
return file_stat_cpy; return file_stat_cpy;
} }
@ -57,6 +60,7 @@ void csync_vio_file_stat_destroy(csync_vio_file_stat_t *file_stat) {
SAFE_FREE(file_stat->directDownloadCookies); SAFE_FREE(file_stat->directDownloadCookies);
SAFE_FREE(file_stat->name); SAFE_FREE(file_stat->name);
SAFE_FREE(file_stat->original_name); SAFE_FREE(file_stat->original_name);
SAFE_FREE(file_stat->checksumHeader);
SAFE_FREE(file_stat); SAFE_FREE(file_stat);
} }

View file

@ -81,6 +81,8 @@ Q_LOGGING_CATEGORY(lcChecksums, "sync.checksums", QtInfoMsg)
QByteArray makeChecksumHeader(const QByteArray &checksumType, const QByteArray &checksum) QByteArray makeChecksumHeader(const QByteArray &checksumType, const QByteArray &checksum)
{ {
if (checksumType.isEmpty() || checksum.isEmpty())
return QByteArray();
QByteArray header = checksumType; QByteArray header = checksumType;
header.append(':'); header.append(':');
header.append(checksum); header.append(checksum);
@ -105,6 +107,16 @@ bool parseChecksumHeader(const QByteArray &header, QByteArray *type, QByteArray
return true; return true;
} }
QByteArray parseChecksumHeaderType(const QByteArray &header)
{
const auto idx = header.indexOf(':');
if (idx < 0) {
return QByteArray();
}
return header.left(idx);
}
bool uploadChecksumEnabled() bool uploadChecksumEnabled()
{ {
static bool enabled = qgetenv("OWNCLOUD_DISABLE_CHECKSUM_UPLOAD").isEmpty(); static bool enabled = qgetenv("OWNCLOUD_DISABLE_CHECKSUM_UPLOAD").isEmpty();
@ -214,39 +226,27 @@ void ValidateChecksumHeader::slotChecksumCalculated(const QByteArray &checksumTy
emit validated(checksumType, checksum); emit validated(checksumType, checksum);
} }
CSyncChecksumHook::CSyncChecksumHook(SyncJournalDb *journal) CSyncChecksumHook::CSyncChecksumHook()
: _journal(journal)
{ {
} }
const char *CSyncChecksumHook::hook(const char *path, uint32_t checksumTypeId, void *this_obj) const char *CSyncChecksumHook::hook(const char *path, const char *otherChecksumHeader, void * /*this_obj*/)
{ {
CSyncChecksumHook *checksumHook = static_cast<CSyncChecksumHook *>(this_obj); QByteArray type = parseChecksumHeaderType(QByteArray(otherChecksumHeader));
QByteArray checksum = checksumHook->compute(QString::fromUtf8(path), checksumTypeId); if (type.isEmpty())
return NULL;
QByteArray checksum = ComputeChecksum::computeNow(path, type);
if (checksum.isNull()) { if (checksum.isNull()) {
qCWarning(lcChecksums) << "Failed to compute checksum" << type << "for" << path;
return NULL; return NULL;
} }
char *result = (char *)malloc(checksum.size() + 1); QByteArray checksumHeader = makeChecksumHeader(type, checksum);
memcpy(result, checksum.constData(), checksum.size()); char *result = (char *)malloc(checksumHeader.size() + 1);
result[checksum.size()] = 0; memcpy(result, checksumHeader.constData(), checksumHeader.size());
result[checksumHeader.size()] = 0;
return result; return result;
} }
QByteArray CSyncChecksumHook::compute(const QString &path, int checksumTypeId)
{
QByteArray checksumType = _journal->getChecksumType(checksumTypeId);
if (checksumType.isEmpty()) {
qCWarning(lcChecksums) << "Checksum type" << checksumTypeId << "not found";
return QByteArray();
}
QByteArray checksum = ComputeChecksum::computeNow(path, checksumType);
if (checksum.isNull()) {
qCWarning(lcChecksums) << "Failed to compute checksum" << checksumType << "for" << path;
return QByteArray();
}
return checksum;
}
} }

View file

@ -31,6 +31,9 @@ QByteArray makeChecksumHeader(const QByteArray &checksumType, const QByteArray &
/// Parses a checksum header /// Parses a checksum header
bool parseChecksumHeader(const QByteArray &header, QByteArray *type, QByteArray *checksum); bool parseChecksumHeader(const QByteArray &header, QByteArray *type, QByteArray *checksum);
/// Convenience for getting the type from a checksum header, null if none
QByteArray parseChecksumHeaderType(const QByteArray &header);
/// Checks OWNCLOUD_DISABLE_CHECKSUM_UPLOAD /// Checks OWNCLOUD_DISABLE_CHECKSUM_UPLOAD
bool uploadChecksumEnabled(); bool uploadChecksumEnabled();
@ -119,20 +122,15 @@ class OWNCLOUDSYNC_EXPORT CSyncChecksumHook : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit CSyncChecksumHook(SyncJournalDb *journal); explicit CSyncChecksumHook();
/** /**
* Returns the checksum value for \a path for the given \a checksumTypeId. * Returns the checksum value for \a path that is comparable to \a otherChecksum.
* *
* Called from csync, where a instance of CSyncChecksumHook has * Called from csync, where a instance of CSyncChecksumHook has
* to be set as userdata. * to be set as userdata.
* The return value will be owned by csync. * The return value will be owned by csync.
*/ */
static const char *hook(const char *path, uint32_t checksumTypeId, void *this_obj); static const char *hook(const char *path, const char *otherChecksumHeader, void *this_obj);
QByteArray compute(const QString &path, int checksumTypeId);
private:
SyncJournalDb *_journal;
}; };
} }

View file

@ -272,7 +272,8 @@ void DiscoverySingleDirectoryJob::start()
<< "http://owncloud.org/ns:id" << "http://owncloud.org/ns:id"
<< "http://owncloud.org/ns:downloadURL" << "http://owncloud.org/ns:downloadURL"
<< "http://owncloud.org/ns:dDC" << "http://owncloud.org/ns:dDC"
<< "http://owncloud.org/ns:permissions"; << "http://owncloud.org/ns:permissions"
<< "http://owncloud.org/ns:checksums";
if (_isRootPath) if (_isRootPath)
props << "http://owncloud.org/ns:data-fingerprint"; props << "http://owncloud.org/ns:data-fingerprint";
@ -294,6 +295,28 @@ void DiscoverySingleDirectoryJob::abort()
} }
} }
/**
* Returns the highest-quality checksum in a 'checksums'
* property retrieved from the server.
*
* Example: "ADLER32:1231 SHA1:ab124124 MD5:2131affa21"
* -> "SHA1:ab124124"
*/
static QByteArray findBestChecksum(const QByteArray &checksums)
{
int i = 0;
// The order of the searches here defines the preference ordering.
if (-1 != (i = checksums.indexOf("SHA1:"))
|| -1 != (i = checksums.indexOf("MD5:"))
|| -1 != (i = checksums.indexOf("Adler32:"))) {
// Now i is the start of the best checksum
// Grab it until the next space or end of string.
auto checksum = checksums.mid(i);
return checksum.mid(0, checksum.indexOf(" "));
}
return QByteArray();
}
static csync_vio_file_stat_t *propertyMapToFileStat(const QMap<QString, QString> &map) static csync_vio_file_stat_t *propertyMapToFileStat(const QMap<QString, QString> &map)
{ {
csync_vio_file_stat_t *file_stat = csync_vio_file_stat_new(); csync_vio_file_stat_t *file_stat = csync_vio_file_stat_new();
@ -343,6 +366,11 @@ static csync_vio_file_stat_t *propertyMapToFileStat(const QMap<QString, QString>
} else { } else {
qCWarning(lcDiscovery) << "permissions too large" << v; qCWarning(lcDiscovery) << "permissions too large" << v;
} }
} else if (property == "checksums") {
QByteArray checksum = findBestChecksum(value.toUtf8());
if (!checksum.isEmpty()) {
file_stat->checksumHeader = strdup(checksum.constData());
}
} }
} }

View file

@ -344,6 +344,41 @@ void PropagateDownloadFile::start()
} }
} }
// If we have a conflict where size and mtime are identical,
// compare the remote checksum to the local one.
// Maybe it's not a real conflict and no download is necessary!
if (_item->_instruction == CSYNC_INSTRUCTION_CONFLICT
&& _item->_size == _item->log._other_size
&& _item->_modtime == _item->log._other_modtime
&& !_item->_checksumHeader.isEmpty()) {
qCDebug(lcPropagateDownload) << _item->_file << "may not need download, computing checksum";
auto computeChecksum = new ComputeChecksum(this);
computeChecksum->setChecksumType(parseChecksumHeaderType(_item->_checksumHeader));
connect(computeChecksum, SIGNAL(done(QByteArray, QByteArray)),
SLOT(conflictChecksumComputed(QByteArray, QByteArray)));
computeChecksum->start(propagator()->getFilePath(_item->_file));
return;
}
startDownload();
}
void PropagateDownloadFile::conflictChecksumComputed(const QByteArray &checksumType, const QByteArray &checksum)
{
if (makeChecksumHeader(checksumType, checksum) == _item->_checksumHeader) {
qCDebug(lcPropagateDownload) << _item->_file << "remote and local checksum match";
// No download necessary, just update metadata
updateMetadata(/*isConflict=*/false);
return;
}
startDownload();
}
void PropagateDownloadFile::startDownload()
{
if (propagator()->_abortRequested.fetchAndAddRelaxed(0))
return;
// do a klaas' case clash check. // do a klaas' case clash check.
if (propagator()->localFileNameClash(_item->_file)) { if (propagator()->localFileNameClash(_item->_file)) {
done(SyncFileItem::NormalError, tr("File %1 can not be downloaded because of a local file name clash!").arg(QDir::toNativeSeparators(_item->_file))); done(SyncFileItem::NormalError, tr("File %1 can not be downloaded because of a local file name clash!").arg(QDir::toNativeSeparators(_item->_file)));
@ -672,7 +707,7 @@ namespace { // Anonymous namespace for the recall feature
continue; continue;
} }
qCInfo(lcPropagateDownload) << "Recalling" << localRecalledFile << "Checksum:" << record._contentChecksumType << record._contentChecksum; qCInfo(lcPropagateDownload) << "Recalling" << localRecalledFile << "Checksum:" << record._checksumHeader;
QString targetPath = makeRecallFileName(recalledFile); QString targetPath = makeRecallFileName(recalledFile);
@ -717,8 +752,7 @@ void PropagateDownloadFile::transmissionChecksumValidated(const QByteArray &chec
void PropagateDownloadFile::contentChecksumComputed(const QByteArray &checksumType, const QByteArray &checksum) void PropagateDownloadFile::contentChecksumComputed(const QByteArray &checksumType, const QByteArray &checksum)
{ {
_item->_contentChecksum = checksum; _item->_checksumHeader = makeChecksumHeader(checksumType, checksum);
_item->_contentChecksumType = checksumType;
downloadFinished(); downloadFinished();
} }
@ -827,6 +861,13 @@ void PropagateDownloadFile::downloadFinished()
// Get up to date information for the journal. // Get up to date information for the journal.
_item->_size = FileSystem::getSize(fn); _item->_size = FileSystem::getSize(fn);
updateMetadata(isConflict);
}
void PropagateDownloadFile::updateMetadata(bool isConflict)
{
QString fn = propagator()->getFilePath(_item->_file);
if (!propagator()->_journal->setFileRecord(SyncJournalFileRecord(*_item, fn))) { if (!propagator()->_journal->setFileRecord(SyncJournalFileRecord(*_item, fn))) {
done(SyncFileItem::FatalError, tr("Error writing metadata to the database")); done(SyncFileItem::FatalError, tr("Error writing metadata to the database"));
return; return;

View file

@ -106,6 +106,40 @@ private slots:
/** /**
* @brief The PropagateDownloadFile class * @brief The PropagateDownloadFile class
* @ingroup libsync * @ingroup libsync
*
* This is the flow:
\code{.unparsed}
start()
|
| deleteExistingFolder() if enabled
|
+--> mtime and size identical?
| then compute the local checksum
| done?-> conflictChecksumComputed()
| |
| checksum differs? |
+-> startDownload() <--------------------------+
| |
+-> run a GETFileJob | checksum identical?
|
done?-> slotGetFinished() |
| |
+-> validate checksum header |
|
done?-> transmissionChecksumValidated() |
| |
+-> compute the content checksum |
|
done?-> contentChecksumComputed() |
| |
+-> downloadFinished() |
| |
+------------------+ |
| |
+-> updateMetadata() <-------------------------+
\endcode
*/ */
class PropagateDownloadFile : public PropagateItemJob class PropagateDownloadFile : public PropagateItemJob
{ {
@ -136,11 +170,22 @@ public:
void setDeleteExistingFolder(bool enabled); void setDeleteExistingFolder(bool enabled);
private slots: private slots:
/// Called when ComputeChecksum on the local file finishes,
/// maybe the local and remote checksums are identical?
void conflictChecksumComputed(const QByteArray &checksumType, const QByteArray &checksum);
/// Called to start downloading the remote file
void startDownload();
/// Called when the GETFileJob finishes
void slotGetFinished(); void slotGetFinished();
void abort() Q_DECL_OVERRIDE; /// Called when the download's checksum header was validated
void transmissionChecksumValidated(const QByteArray &checksumType, const QByteArray &checksum); void transmissionChecksumValidated(const QByteArray &checksumType, const QByteArray &checksum);
/// Called when the download's checksum computation is done
void contentChecksumComputed(const QByteArray &checksumType, const QByteArray &checksum); void contentChecksumComputed(const QByteArray &checksumType, const QByteArray &checksum);
void downloadFinished(); void downloadFinished();
/// Called when it's time to update the db metadata
void updateMetadata(bool isConflict);
void abort() Q_DECL_OVERRIDE;
void slotDownloadProgress(qint64, qint64); void slotDownloadProgress(qint64, qint64);
void slotChecksumFail(const QString &errMsg); void slotChecksumFail(const QString &errMsg);

View file

@ -173,8 +173,7 @@ void PropagateRemoteMove::finalize()
SyncJournalFileRecord record(*_item, propagator()->getFilePath(_item->_renameTarget)); SyncJournalFileRecord record(*_item, propagator()->getFilePath(_item->_renameTarget));
record._path = _item->_renameTarget; record._path = _item->_renameTarget;
if (oldRecord.isValid()) { if (oldRecord.isValid()) {
record._contentChecksum = oldRecord._contentChecksum; record._checksumHeader = oldRecord._checksumHeader;
record._contentChecksumType = oldRecord._contentChecksumType;
if (record._fileSize != oldRecord._fileSize) { if (record._fileSize != oldRecord._fileSize) {
qCWarning(lcPropagateRemoteMove) << "File sizes differ on server vs sync journal: " << record._fileSize << oldRecord._fileSize; qCWarning(lcPropagateRemoteMove) << "File sizes differ on server vs sync journal: " << record._fileSize << oldRecord._fileSize;

View file

@ -231,9 +231,10 @@ void PropagateUploadFileCommon::slotComputeContentChecksum()
QByteArray checksumType = contentChecksumType(); QByteArray checksumType = contentChecksumType();
// Maybe the discovery already computed the checksum? // Maybe the discovery already computed the checksum?
if (_item->_contentChecksumType == checksumType QByteArray existingChecksumType, existingChecksum;
&& !_item->_contentChecksum.isEmpty()) { parseChecksumHeader(_item->_checksumHeader, &existingChecksumType, &existingChecksum);
slotComputeTransmissionChecksum(checksumType, _item->_contentChecksum); if (existingChecksumType == checksumType) {
slotComputeTransmissionChecksum(checksumType, existingChecksum);
return; return;
} }
@ -250,8 +251,7 @@ void PropagateUploadFileCommon::slotComputeContentChecksum()
void PropagateUploadFileCommon::slotComputeTransmissionChecksum(const QByteArray &contentChecksumType, const QByteArray &contentChecksum) void PropagateUploadFileCommon::slotComputeTransmissionChecksum(const QByteArray &contentChecksumType, const QByteArray &contentChecksum)
{ {
_item->_contentChecksum = contentChecksum; _item->_checksumHeader = makeChecksumHeader(contentChecksumType, contentChecksum);
_item->_contentChecksumType = contentChecksumType;
#ifdef WITH_TESTING #ifdef WITH_TESTING
_stopWatch.addLapTime(QLatin1String("ContentChecksum")); _stopWatch.addLapTime(QLatin1String("ContentChecksum"));
@ -288,13 +288,11 @@ void PropagateUploadFileCommon::slotStartUpload(const QByteArray &transmissionCh
// When we start chunks, we will add it again, once for every chunks. // When we start chunks, we will add it again, once for every chunks.
propagator()->_activeJobList.removeOne(this); propagator()->_activeJobList.removeOne(this);
_transmissionChecksum = transmissionChecksum; _transmissionChecksumHeader = makeChecksumHeader(transmissionChecksumType, transmissionChecksum);
_transmissionChecksumType = transmissionChecksumType;
if (_item->_contentChecksum.isEmpty() && _item->_contentChecksumType.isEmpty()) { // If no checksum header was not set, reuse the transmission checksum as the content checksum.
// If the _contentChecksum was not set, reuse the transmission checksum as the content checksum. if (_item->_checksumHeader.isEmpty()) {
_item->_contentChecksum = transmissionChecksum; _item->_checksumHeader = _transmissionChecksumHeader;
_item->_contentChecksumType = transmissionChecksumType;
} }
const QString fullFilePath = propagator()->getFilePath(_item->_file); const QString fullFilePath = propagator()->getFilePath(_item->_file);

View file

@ -226,8 +226,7 @@ protected:
Utility::StopWatch _stopWatch; Utility::StopWatch _stopWatch;
#endif #endif
QByteArray _transmissionChecksum; QByteArray _transmissionChecksumHeader;
QByteArray _transmissionChecksumType;
public: public:
PropagateUploadFileCommon(OwncloudPropagator *propagator, const SyncFileItemPtr &item) PropagateUploadFileCommon(OwncloudPropagator *propagator, const SyncFileItemPtr &item)

View file

@ -283,9 +283,8 @@ void PropagateUploadFileNG::startNextChunk()
if (!ifMatch.isEmpty()) { if (!ifMatch.isEmpty()) {
headers["If"] = "<" + destination.toUtf8() + "> ([" + ifMatch + "])"; headers["If"] = "<" + destination.toUtf8() + "> ([" + ifMatch + "])";
} }
if (!_transmissionChecksumType.isEmpty()) { if (!_transmissionChecksumHeader.isEmpty()) {
headers[checkSumHeaderC] = makeChecksumHeader( headers[checkSumHeaderC] = _transmissionChecksumHeader;
_transmissionChecksumType, _transmissionChecksum);
} }
headers["OC-Total-Length"] = QByteArray::number(fileSize); headers["OC-Total-Length"] = QByteArray::number(fileSize);

View file

@ -103,9 +103,8 @@ void PropagateUploadFileV1::startNextChunk()
} }
qCDebug(lcPropagateUpload) << _chunkCount << isFinalChunk << chunkStart << currentChunkSize; qCDebug(lcPropagateUpload) << _chunkCount << isFinalChunk << chunkStart << currentChunkSize;
if (isFinalChunk && !_transmissionChecksumType.isEmpty()) { if (isFinalChunk && !_transmissionChecksumHeader.isEmpty()) {
headers[checkSumHeaderC] = makeChecksumHeader( headers[checkSumHeaderC] = _transmissionChecksumHeader;
_transmissionChecksumType, _transmissionChecksum);
} }
const QString fileName = propagator()->getFilePath(_item->_file); const QString fileName = propagator()->getFilePath(_item->_file);

View file

@ -241,8 +241,7 @@ void PropagateLocalRename::start()
SyncJournalFileRecord record(*_item, targetFile); SyncJournalFileRecord record(*_item, targetFile);
record._path = _item->_renameTarget; record._path = _item->_renameTarget;
if (oldRecord.isValid()) { if (oldRecord.isValid()) {
record._contentChecksum = oldRecord._contentChecksum; record._checksumHeader = oldRecord._checksumHeader;
record._contentChecksumType = oldRecord._contentChecksumType;
} }
if (!_item->_isDirectory) { // Directories are saved at the end if (!_item->_isDirectory) { // Directories are saved at the end

View file

@ -73,7 +73,6 @@ SyncEngine::SyncEngine(AccountPtr account, const QString &localPath,
, _backInTimeFiles(0) , _backInTimeFiles(0)
, _uploadLimit(0) , _uploadLimit(0)
, _downloadLimit(0) , _downloadLimit(0)
, _checksum_hook(journal)
, _anotherSyncNeeded(NoFollowUpSync) , _anotherSyncNeeded(NoFollowUpSync)
{ {
qRegisterMetaType<SyncFileItem>("SyncFileItem"); qRegisterMetaType<SyncFileItem>("SyncFileItem");
@ -426,9 +425,12 @@ int SyncEngine::treewalkFile(TREE_WALK_FILE *file, bool remote)
} }
// Sometimes the discovery computes checksums for local files // Sometimes the discovery computes checksums for local files
if (!remote && file->checksum && file->checksumTypeId) { if (!remote && file->checksumHeader) {
item->_contentChecksum = QByteArray(file->checksum); item->_checksumHeader = QByteArray(file->checksumHeader);
item->_contentChecksumType = _journal->getChecksumType(file->checksumTypeId); }
// For conflicts, store the remote checksum there
if (remote && item->_instruction == CSYNC_INSTRUCTION_CONFLICT && file->checksumHeader) {
item->_checksumHeader = QByteArray(file->checksumHeader);
} }
// record the seen files to be able to clean the journal later // record the seen files to be able to clean the journal later

View file

@ -190,8 +190,13 @@ public:
quint64 _inode; quint64 _inode;
QByteArray _fileId; QByteArray _fileId;
QByteArray _remotePerm; QByteArray _remotePerm;
QByteArray _contentChecksum;
QByteArray _contentChecksumType; // When is this set, and is it the local or the remote checksum?
// - if mtime or size changed locally for *.eml files (local checksum)
// - for potential renames of local files (local checksum)
// - for conflicts (remote checksum) (what about eval_rename/new reconcile?)
QByteArray _checksumHeader;
QString _directDownloadUrl; QString _directDownloadUrl;
QString _directDownloadCookies; QString _directDownloadCookies;

View file

@ -28,6 +28,7 @@
#include "version.h" #include "version.h"
#include "filesystem.h" #include "filesystem.h"
#include "asserts.h" #include "asserts.h"
#include "checksums.h"
#include "../../csync/src/std/c_jhash.h" #include "../../csync/src/std/c_jhash.h"
@ -448,7 +449,7 @@ bool SyncJournalDb::checkConnect()
_getFileRecordQuery.reset(new SqlQuery(_db)); _getFileRecordQuery.reset(new SqlQuery(_db));
if (_getFileRecordQuery->prepare( if (_getFileRecordQuery->prepare(
"SELECT path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize," "SELECT path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize,"
" ignoredChildrenRemote, contentChecksum, contentchecksumtype.name" " ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum"
" FROM metadata" " FROM metadata"
" LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id" " LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
" WHERE phash=?1")) { " WHERE phash=?1")) {
@ -832,7 +833,7 @@ bool SyncJournalDb::setFileRecord(const SyncJournalFileRecord &_record)
qCInfo(lcDb) << "Updating file record for path:" << record._path << "inode:" << record._inode qCInfo(lcDb) << "Updating file record for path:" << record._path << "inode:" << record._inode
<< "modtime:" << record._modtime << "type:" << record._type << "modtime:" << record._modtime << "type:" << record._type
<< "etag:" << record._etag << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm << "etag:" << record._etag << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm
<< "fileSize:" << record._fileSize << "checksum:" << record._contentChecksum << record._contentChecksumType; << "fileSize:" << record._fileSize << "checksum:" << record._checksumHeader;
qlonglong phash = getPHash(record._path); qlonglong phash = getPHash(record._path);
if (checkConnect()) { if (checkConnect()) {
@ -848,7 +849,9 @@ bool SyncJournalDb::setFileRecord(const SyncJournalFileRecord &_record)
QString remotePerm(record._remotePerm); QString remotePerm(record._remotePerm);
if (remotePerm.isEmpty()) if (remotePerm.isEmpty())
remotePerm = QString(); // have NULL in DB (vs empty) remotePerm = QString(); // have NULL in DB (vs empty)
int contentChecksumTypeId = mapChecksumType(record._contentChecksumType); QByteArray checksumType, checksum;
parseChecksumHeader(record._checksumHeader, &checksumType, &checksum);
int contentChecksumTypeId = mapChecksumType(checksumType);
_setFileRecordQuery->reset_and_clear_bindings(); _setFileRecordQuery->reset_and_clear_bindings();
_setFileRecordQuery->bindValue(1, QString::number(phash)); _setFileRecordQuery->bindValue(1, QString::number(phash));
_setFileRecordQuery->bindValue(2, plen); _setFileRecordQuery->bindValue(2, plen);
@ -864,7 +867,7 @@ bool SyncJournalDb::setFileRecord(const SyncJournalFileRecord &_record)
_setFileRecordQuery->bindValue(12, remotePerm); _setFileRecordQuery->bindValue(12, remotePerm);
_setFileRecordQuery->bindValue(13, record._fileSize); _setFileRecordQuery->bindValue(13, record._fileSize);
_setFileRecordQuery->bindValue(14, record._serverHasIgnoredFiles ? 1 : 0); _setFileRecordQuery->bindValue(14, record._serverHasIgnoredFiles ? 1 : 0);
_setFileRecordQuery->bindValue(15, record._contentChecksum); _setFileRecordQuery->bindValue(15, checksum);
_setFileRecordQuery->bindValue(16, contentChecksumTypeId); _setFileRecordQuery->bindValue(16, contentChecksumTypeId);
if (!_setFileRecordQuery->exec()) { if (!_setFileRecordQuery->exec()) {
@ -943,10 +946,7 @@ SyncJournalFileRecord SyncJournalDb::getFileRecord(const QString &filename)
rec._remotePerm = _getFileRecordQuery->baValue(9); rec._remotePerm = _getFileRecordQuery->baValue(9);
rec._fileSize = _getFileRecordQuery->int64Value(10); rec._fileSize = _getFileRecordQuery->int64Value(10);
rec._serverHasIgnoredFiles = (_getFileRecordQuery->intValue(11) > 0); rec._serverHasIgnoredFiles = (_getFileRecordQuery->intValue(11) > 0);
rec._contentChecksum = _getFileRecordQuery->baValue(12); rec._checksumHeader = _getFileRecordQuery->baValue(12);
if (!_getFileRecordQuery->nullValue(13)) {
rec._contentChecksumType = _getFileRecordQuery->baValue(13);
}
_getFileRecordQuery->reset_and_clear_bindings(); _getFileRecordQuery->reset_and_clear_bindings();
} else { } else {
int errId = _getFileRecordQuery->errorId(); int errId = _getFileRecordQuery->errorId();

View file

@ -47,8 +47,7 @@ SyncJournalFileRecord::SyncJournalFileRecord(const SyncFileItem &item, const QSt
, _fileSize(item._size) , _fileSize(item._size)
, _remotePerm(item._remotePerm) , _remotePerm(item._remotePerm)
, _serverHasIgnoredFiles(item._serverHasIgnoredFiles) , _serverHasIgnoredFiles(item._serverHasIgnoredFiles)
, _contentChecksum(item._contentChecksum) , _checksumHeader(item._checksumHeader)
, _contentChecksumType(item._contentChecksumType)
{ {
// use the "old" inode coming with the item for the case where the // use the "old" inode coming with the item for the case where the
// filesystem stat fails. That can happen if the the file was removed // filesystem stat fails. That can happen if the the file was removed
@ -106,8 +105,7 @@ SyncFileItem SyncJournalFileRecord::toSyncFileItem()
item._size = _fileSize; item._size = _fileSize;
item._remotePerm = _remotePerm; item._remotePerm = _remotePerm;
item._serverHasIgnoredFiles = _serverHasIgnoredFiles; item._serverHasIgnoredFiles = _serverHasIgnoredFiles;
item._contentChecksum = _contentChecksum; item._checksumHeader = _checksumHeader;
item._contentChecksumType = _contentChecksumType;
return item; return item;
} }
@ -130,7 +128,6 @@ bool operator==(const SyncJournalFileRecord &lhs,
&& lhs._fileSize == rhs._fileSize && lhs._fileSize == rhs._fileSize
&& lhs._remotePerm == rhs._remotePerm && lhs._remotePerm == rhs._remotePerm
&& lhs._serverHasIgnoredFiles == rhs._serverHasIgnoredFiles && lhs._serverHasIgnoredFiles == rhs._serverHasIgnoredFiles
&& lhs._contentChecksum == rhs._contentChecksum && lhs._checksumHeader == rhs._checksumHeader;
&& lhs._contentChecksumType == rhs._contentChecksumType;
} }
} }

View file

@ -57,8 +57,7 @@ public:
qint64 _fileSize; qint64 _fileSize;
QByteArray _remotePerm; QByteArray _remotePerm;
bool _serverHasIgnoredFiles; bool _serverHasIgnoredFiles;
QByteArray _contentChecksum; QByteArray _checksumHeader;
QByteArray _contentChecksumType;
}; };
bool OWNCLOUDSYNC_EXPORT bool OWNCLOUDSYNC_EXPORT

View file

@ -321,6 +321,70 @@ private slots:
} }
} }
void testFakeConflict()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
int nGET = 0;
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &) {
if (op == QNetworkAccessManager::GetOperation)
++nGET;
return nullptr;
});
// For directly editing the remote checksum
FileInfo &remoteInfo = dynamic_cast<FileInfo &>(fakeFolder.remoteModifier());
// Base mtime with no ms content (filesystem is seconds only)
auto mtime = QDateTime::currentDateTime().addDays(-4);
mtime.setMSecsSinceEpoch(mtime.toMSecsSinceEpoch() / 1000 * 1000);
// Conflict: Same content, mtime, but no server checksum
// -> ignored in reconcile
fakeFolder.localModifier().setContents("A/a1", 'C');
fakeFolder.localModifier().setModTime("A/a1", mtime);
fakeFolder.remoteModifier().setContents("A/a1", 'C');
fakeFolder.remoteModifier().setModTime("A/a1", mtime);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 0);
// Conflict: Same content, mtime, but weak server checksum
// -> ignored in reconcile
mtime = mtime.addDays(1);
fakeFolder.localModifier().setContents("A/a1", 'D');
fakeFolder.localModifier().setModTime("A/a1", mtime);
fakeFolder.remoteModifier().setContents("A/a1", 'D');
fakeFolder.remoteModifier().setModTime("A/a1", mtime);
remoteInfo.find("A/a1")->checksums = "Adler32:bad";
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 0);
// Conflict: Same content, mtime, but server checksum differs
// -> downloaded
mtime = mtime.addDays(1);
fakeFolder.localModifier().setContents("A/a1", 'W');
fakeFolder.localModifier().setModTime("A/a1", mtime);
fakeFolder.remoteModifier().setContents("A/a1", 'W');
fakeFolder.remoteModifier().setModTime("A/a1", mtime);
remoteInfo.find("A/a1")->checksums = "SHA1:bad";
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 1);
// Conflict: Same content, mtime, matching checksums
// -> PropagateDownload, but it skips the download
mtime = mtime.addDays(1);
fakeFolder.localModifier().setContents("A/a1", 'C');
fakeFolder.localModifier().setModTime("A/a1", mtime);
fakeFolder.remoteModifier().setContents("A/a1", 'C');
fakeFolder.remoteModifier().setModTime("A/a1", mtime);
remoteInfo.find("A/a1")->checksums = "SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427";
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 1);
// Extra sync reads from db, no difference
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 1);
}
}; };
QTEST_GUILESS_MAIN(TestSyncEngine) QTEST_GUILESS_MAIN(TestSyncEngine)

View file

@ -53,17 +53,15 @@ private slots:
record._fileId = "abcd"; record._fileId = "abcd";
record._remotePerm = "744"; record._remotePerm = "744";
record._fileSize = 213089055; record._fileSize = 213089055;
record._contentChecksum = "mychecksum"; record._checksumHeader = "MD5:mychecksum";
record._contentChecksumType = "MD5";
QVERIFY(_db.setFileRecord(record)); QVERIFY(_db.setFileRecord(record));
SyncJournalFileRecord storedRecord = _db.getFileRecord("foo"); SyncJournalFileRecord storedRecord = _db.getFileRecord("foo");
QVERIFY(storedRecord == record); QVERIFY(storedRecord == record);
// Update checksum // Update checksum
record._contentChecksum = "newchecksum"; record._checksumHeader = "Adler32:newchecksum";
record._contentChecksumType = "Adler32"; _db.updateFileRecordChecksum("foo", "newchecksum", "Adler32");
_db.updateFileRecordChecksum("foo", record._contentChecksum, record._contentChecksumType);
storedRecord = _db.getFileRecord("foo"); storedRecord = _db.getFileRecord("foo");
QVERIFY(storedRecord == record); QVERIFY(storedRecord == record);
@ -91,16 +89,14 @@ private slots:
SyncJournalFileRecord record; SyncJournalFileRecord record;
record._path = "foo-checksum"; record._path = "foo-checksum";
record._remotePerm = "744"; record._remotePerm = "744";
record._contentChecksum = "mychecksum"; record._checksumHeader = "MD5:mychecksum";
record._contentChecksumType = "MD5";
record._modtime = QDateTime::currentDateTimeUtc(); record._modtime = QDateTime::currentDateTimeUtc();
QVERIFY(_db.setFileRecord(record)); QVERIFY(_db.setFileRecord(record));
SyncJournalFileRecord storedRecord = _db.getFileRecord("foo-checksum"); SyncJournalFileRecord storedRecord = _db.getFileRecord("foo-checksum");
QVERIFY(storedRecord._path == record._path); QVERIFY(storedRecord._path == record._path);
QVERIFY(storedRecord._remotePerm == record._remotePerm); QVERIFY(storedRecord._remotePerm == record._remotePerm);
QVERIFY(storedRecord._contentChecksum == record._contentChecksum); QVERIFY(storedRecord._checksumHeader == record._checksumHeader);
QVERIFY(storedRecord._contentChecksumType == record._contentChecksumType);
// qDebug()<< "OOOOO " << storedRecord._modtime.toTime_t() << record._modtime.toTime_t(); // qDebug()<< "OOOOO " << storedRecord._modtime.toTime_t() << record._modtime.toTime_t();