Files
sqlitebrowser/src/RemoteDock.cpp
Martin Kleusberg 69eec2b602 dbhub: Redownload database when checking out current commit
When double clicking the currently checked out commit in the commits
list in the Remote dock only open the local file if it has not been
modified. If it was modified show a warning about the modifications and
redownload the file if the user decides to proceed.

See issue #2349.
2020-08-12 20:26:50 +02:00

671 lines
29 KiB
C++

#include <QClipboard>
#include <QCryptographicHash>
#include <QDesktopServices>
#include <QFileInfo>
#include <QInputDialog>
#include <QMessageBox>
#include <QUrl>
#include <QUrlQuery>
#include <json.hpp>
#include "RemoteDock.h"
#include "ui_RemoteDock.h"
#include "Settings.h"
#include "RemoteCommitsModel.h"
#include "RemoteDatabase.h"
#include "RemoteLocalFilesModel.h"
#include "RemoteModel.h"
#include "MainWindow.h"
#include "RemotePushDialog.h"
#include "PreferencesDialog.h"
using json = nlohmann::json;
RemoteDock::RemoteDock(MainWindow* parent)
: QDialog(parent),
ui(new Ui::RemoteDock),
mainWindow(parent),
remoteModel(new RemoteModel(this)),
remoteLocalFilesModel(new RemoteLocalFilesModel(this, remoteDatabase)),
remoteCommitsModel(new RemoteCommitsModel(this))
{
ui->setupUi(this);
// Set models
ui->treeRemote->setModel(remoteModel);
ui->treeLocal->setModel(remoteLocalFilesModel);
ui->treeDatabaseCommits->setModel(remoteCommitsModel);
// Set initial column widths for tree views
ui->treeRemote->setColumnWidth(0, 300); // Make name column wider
ui->treeRemote->setColumnWidth(2, 80); // Make size column narrower
ui->treeLocal->setColumnWidth(RemoteLocalFilesModel::ColumnName, 300); // Make name column wider
ui->treeLocal->setColumnWidth(RemoteLocalFilesModel::ColumnSize, 80); // Make size column narrower
ui->treeLocal->setColumnHidden(RemoteLocalFilesModel::ColumnFile, true); // Hide local file name
// Handle finished uploads and downloads of databases
connect(&RemoteNetwork::get(), &RemoteNetwork::fetchFinished, this, &RemoteDock::fetchFinished);
connect(&RemoteNetwork::get(), &RemoteNetwork::pushFinished, this, &RemoteDock::pushFinished);
// Whenever a new directory listing has been parsed, check if it was a new root dir and, if so, open the user's directory
connect(remoteModel, &RemoteModel::directoryListingParsed, this, &RemoteDock::newDirectoryNode);
// When the Preferences link is clicked in the no-certificates-label, open the preferences dialog. For other links than the ones we know,
// just open them in a web browser
connect(ui->labelNoCert, &QLabel::linkActivated, [this](const QString& link) {
if(link == "#preferences")
{
PreferencesDialog dialog(mainWindow, PreferencesDialog::TabRemote);
if(dialog.exec())
mainWindow->reloadSettings();
} else {
QDesktopServices::openUrl(QUrl(link));
}
});
// When changing the current branch in the branches combo box, update the tree view accordingly
connect(ui->comboDatabaseBranch, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [this](int /*index*/) {
remoteCommitsModel->refresh(current_commit_json, ui->comboDatabaseBranch->currentData().toString().toStdString(), currently_opened_file_info.commit_id);
ui->treeDatabaseCommits->expandAll();
});
// Fetch latest commit action
connect(ui->actionFetchLatestCommit, &QAction::triggered, [this]() {
// Fetch last commit of current branch
// The URL and the branch name is that of the currently opened database file.
// The latest commit id is stored in the data bits of the branch combo box.
QUrl url(QString::fromStdString(currently_opened_file_info.url));
QUrlQuery query;
query.addQueryItem("branch", QString::fromStdString(currently_opened_file_info.branch));
query.addQueryItem("commit", ui->comboDatabaseBranch->itemData(
ui->comboDatabaseBranch->findText(QString::fromStdString(currently_opened_file_info.branch))).toString());
url.setQuery(query);
fetchDatabase(url.toString());
});
// Prepare context menu for list of remote databases
connect(ui->treeRemote->selectionModel(), &QItemSelectionModel::currentChanged, [this](const QModelIndex& index, const QModelIndex&) {
// Only enable database actions when a database was selected
bool enable = index.isValid() &&
remoteModel->modelIndexToItem(index)->value(RemoteModelColumnType).toString() == "database";
ui->actionCloneDatabaseDoubleClick->setEnabled(enable);
});
ui->treeRemote->selectionModel()->currentChanged(QModelIndex(), QModelIndex()); // Enable/disable all action initially
connect(ui->actionCloneDatabaseDoubleClick, &QAction::triggered, [this]() {
fetchDatabase(ui->treeRemote->currentIndex());
});
ui->treeRemote->addAction(ui->actionCloneDatabaseDoubleClick);
// Prepare context menu for list of local clones
connect(ui->treeLocal->selectionModel(), &QItemSelectionModel::currentChanged, [this](const QModelIndex& index, const QModelIndex&) {
// Only enable database actions when a database was selected
bool enable = index.isValid() &&
!index.sibling(index.row(), RemoteLocalFilesModel::ColumnFile).data().isNull();
ui->actionOpenLocalDatabase->setEnabled(enable);
ui->actionPushLocalDatabase->setEnabled(enable);
ui->actionDeleteDatabase->setEnabled(enable);
});
ui->treeLocal->selectionModel()->currentChanged(QModelIndex(), QModelIndex()); // Enable/disable all action initially
connect(ui->actionOpenLocalDatabase, &QAction::triggered, [this]() {
openLocalFile(ui->treeLocal->currentIndex());
});
connect(ui->actionDeleteDatabase, &QAction::triggered, [this]() {
deleteLocalDatabase(ui->treeLocal->currentIndex());
});
ui->treeLocal->addAction(ui->actionOpenLocalDatabase);
ui->treeLocal->addAction(ui->actionPushLocalDatabase);
ui->treeLocal->addAction(ui->actionDeleteDatabase);
// Prepare context menu for list of commits
connect(ui->treeDatabaseCommits->selectionModel(), &QItemSelectionModel::currentChanged, [this](const QModelIndex& index, const QModelIndex&) {
// Only enable database actions when a commit was selected
bool enable = index.isValid();
ui->actionFetchCommit->setEnabled(enable);
ui->actionDownloadCommit->setEnabled(enable);
});
ui->treeDatabaseCommits->selectionModel()->currentChanged(QModelIndex(), QModelIndex()); // Enable/disable all action initially
connect(ui->actionFetchCommit, &QAction::triggered, [this]() {
fetchCommit(ui->treeDatabaseCommits->currentIndex());
});
connect(ui->actionDownloadCommit, &QAction::triggered, [this]() {
fetchCommit(ui->treeDatabaseCommits->currentIndex(), RemoteNetwork::RequestTypeDownload);
});
ui->treeDatabaseCommits->addAction(ui->actionFetchCommit);
ui->treeDatabaseCommits->addAction(ui->actionDownloadCommit);
// Initial setup
reloadSettings();
}
RemoteDock::~RemoteDock()
{
delete ui;
}
void RemoteDock::reloadSettings()
{
// Clear list of client certificates and add a dummy entry which does nothing except serve as
// an explanation to the user.
ui->comboUser->clear();
ui->comboUser->addItem(tr("Select an identity to connect"), "dummy");
// Load list of client certs
QStringList client_certs = Settings::getValue("remote", "client_certificates").toStringList();
for(const QString& file : client_certs)
{
auto certs = QSslCertificate::fromPath(file);
for(const QSslCertificate& cert : certs)
ui->comboUser->addItem(cert.subjectInfo(QSslCertificate::CommonName).at(0), file);
}
// Add public certificate for anonymous read-only access to dbhub.io
ui->comboUser->addItem(tr("Public"), ":/user_certs/public.cert.pem");
}
void RemoteDock::setNewIdentity(const QString& identity)
{
// Do nothing if the dummy entry was selected
if(ui->comboUser->currentData() == "dummy")
return;
// Check if the dummy item is still there and remove it if it is
if(ui->comboUser->itemData(0) == "dummy")
{
ui->comboUser->blockSignals(true);
ui->comboUser->removeItem(0);
ui->comboUser->blockSignals(false);
}
// Get certificate file name
QString cert = ui->comboUser->itemData(ui->comboUser->findText(identity), Qt::UserRole).toString();
if(cert.isEmpty())
return;
// Open root directory. Get host name from client cert
remoteModel->setNewRootDir(RemoteNetwork::get().getInfoFromClientCert(cert, RemoteNetwork::CertInfoServer), cert);
// Reset list of local checkouts
remoteLocalFilesModel->setIdentity(cert);
refreshLocalFileList();
// Enable buttons if necessary
enableButtons();
}
void RemoteDock::fetchDatabase(const QModelIndex& idx)
{
if(!idx.isValid())
return;
// Get item
const RemoteModelItem* item = remoteModel->modelIndexToItem(idx);
// Only open database file
if(item->value(RemoteModelColumnType).toString() == "database")
fetchDatabase(item->value(RemoteModelColumnUrl).toString());
}
void RemoteDock::fetchDatabase(QString url_string, RemoteNetwork::RequestType request_type)
{
// If no URL was provided ask the user. Default to the current clipboard contents
if(url_string.isEmpty())
{
url_string = QInputDialog::getText(this,
qApp->applicationName(),
tr("This downloads a database from a remote server for local editing.\n"
"Please enter the URL to clone from. You can generate this URL by\n"
"clicking the 'Clone Database in DB4S' button on the web page\n"
"of the database."),
QLineEdit::Normal,
QApplication::clipboard()->text());
}
if(url_string.isEmpty())
return;
// Check the URL
QUrl url(url_string);
if(url.authority() != QUrl(RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoServer)).authority())
{
QMessageBox::warning(this, qApp->applicationName(), tr("Invalid URL: The host name does not match the host name of the current identity."));
return;
}
if(!QUrlQuery(url).hasQueryItem("branch"))
{
QMessageBox::warning(this, qApp->applicationName(), tr("Invalid URL: No branch name specified."));
return;
}
if(!QUrlQuery(url).hasQueryItem("commit"))
{
QMessageBox::warning(this, qApp->applicationName(), tr("Invalid URL: No commit ID specified."));
return;
}
// For the user name, take the path, remove the database name and the initial slash
QString username = url.path().remove("/" + url.fileName()).mid(1);
// There is a chance that we've already cloned that database. So check for that first
QString exists = remoteDatabase.localExists(url, remoteModel->currentClientCertificate(), QUrlQuery(url).queryItemValue("branch").toStdString());
if(!exists.isEmpty() && request_type == RemoteNetwork::RequestTypeDatabase)
{
// Check for modifications
bool modified = isLocalDatabaseModified(exists, username, url.fileName(), remoteModel->currentClientCertificate(),
QUrlQuery(url).queryItemValue("commit").toStdString());
// Database has already been cloned! So open the local file instead of fetching the one from the
// server again. If the local file has been modified don't open it but try to download the last known
// commit again.
if(!modified)
{
emit openFile(exists);
return;
}
}
// Check if we already have a clone of this database branch and, if so, figure out its local file name.
// For this we do not care about the currently checked out commit id because we only have one file per
// database and branch on the disk.
QUrl url_without_commit_id(url);
QUrlQuery url_without_commit_id_query(url_without_commit_id);
url_without_commit_id_query.removeQueryItem("commit");
url_without_commit_id.setQuery(url_without_commit_id_query);
QString local_file = remoteDatabase.localExists(url_without_commit_id, remoteModel->currentClientCertificate(), QUrlQuery(url).queryItemValue("branch").toStdString());
if(request_type == RemoteNetwork::RequestTypeDatabase && !local_file.isEmpty())
{
// If there is a local clone of this dtabase and branch, figure out if the local file has been modified
// Get the last local commit id
std::string last_commit_id = remoteDatabase.localLastCommitId(remoteModel->currentClientCertificate(), url, QUrlQuery(url).queryItemValue("branch").toStdString());
// Check for modifications
bool modified = isLocalDatabaseModified(local_file, username, url.fileName(), remoteModel->currentClientCertificate(), last_commit_id);
// Only if the local file has been modified show a warning that checking out this commit overrides local changes
if(modified)
{
if(QMessageBox::warning(nullptr,
QApplication::applicationName(),
tr("You have modified the local clone of the database. Fetching this commit overrides these local changes.\n"
"Are you sure you want to proceed?"),
QMessageBox::Yes | QMessageBox::Cancel,
QMessageBox::Cancel) == QMessageBox::Cancel)
{
return;
}
}
}
// Clone the database
RemoteNetwork::get().fetch(url.toString(), request_type, remoteModel->currentClientCertificate());
}
void RemoteDock::fetchCommit(const QModelIndex& idx, RemoteNetwork::RequestType request_type)
{
// Fetch selected commit
QUrl url(QString::fromStdString(currently_opened_file_info.url));
QUrlQuery query;
query.addQueryItem("branch", ui->comboDatabaseBranch->currentText());
query.addQueryItem("commit", idx.sibling(idx.row(), RemoteCommitsModel::ColumnCommitId).data().toString());
url.setQuery(query);
fetchDatabase(url.toString(), request_type);
}
void RemoteDock::enableButtons()
{
bool db_opened = mainWindow->getDb().isOpen() && mainWindow->getDb().currentFile() != ":memory:";
bool logged_in = !remoteModel->currentClientCertificate().isEmpty();
ui->buttonPushDatabase->setEnabled(db_opened && logged_in);
ui->actionRefresh->setEnabled(logged_in);
ui->actionCloneDatabaseLink->setEnabled(logged_in);
ui->actionDatabaseOpenBrowser->setEnabled(db_opened && logged_in);
ui->actionFetchLatestCommit->setEnabled(db_opened && logged_in);
}
void RemoteDock::pushCurrentlyOpenedDatabase()
{
// Show a warning when trying to push a database with unsaved changes
if(mainWindow->getDb().getDirty())
{
if(QMessageBox::warning(this,
QApplication::applicationName(),
tr("The database has unsaved changes. Are you sure you want to push it before saving?"),
QMessageBox::Yes | QMessageBox::Cancel,
QMessageBox::Cancel) == QMessageBox::Cancel)
return;
}
// Push currently opened file
pushDatabase(mainWindow->getDb().currentFile(), QString::fromStdString(currently_opened_file_info.branch));
}
void RemoteDock::pushSelectedLocalDatabase()
{
// Return if no file is selected
if(!ui->treeLocal->currentIndex().isValid())
return;
const int row = ui->treeLocal->currentIndex().row();
const QString filename = ui->treeLocal->currentIndex().sibling(row, RemoteLocalFilesModel::ColumnFile).data().toString();
if(filename.isEmpty())
return;
// Push selected file
const QString branch = ui->treeLocal->currentIndex().sibling(row, RemoteLocalFilesModel::ColumnBranch).data().toString();
pushDatabase(Settings::getValue("remote", "clonedirectory").toString() + "/" + filename, branch);
}
void RemoteDock::pushDatabase(const QString& path, const QString& branch)
{
// If the currently active identity is the read-only public access to dbhub.io, don't show the Push Database dialog because it won't work anyway.
// Instead switch to an explanation offering some advice to create and import a proper certificate.
if(remoteModel->currentClientCertificate() == ":/user_certs/public.cert.pem")
{
ui->stack->setCurrentIndex(1);
return;
}
// The default suggestion for a database name is the local file name. If it is a remote file (like when it initially was fetched using DB4S),
// the extra bit of information at the end of the name gets removed first.
QString name = QFileInfo(path).fileName();
name = name.remove(QRegExp("_[0-9]+.remotedb$"));
// Show the user a dialog for setting all the commit details
QString host = RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoServer);
RemotePushDialog pushDialog(this, host, remoteModel->currentClientCertificate(), name, branch);
if(pushDialog.exec() != QDialog::Accepted)
return;
// Build push URL
QString url = host;
url.append(RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoUser));
url.append("/");
url.append(pushDialog.name());
// Check if we are pushing a cloned database. Only in this case we provide the last known commit id
QString commit_id;
if(path.startsWith(Settings::getValue("remote", "clonedirectory").toString()))
commit_id = QString::fromStdString(remoteDatabase.localLastCommitId(remoteModel->currentClientCertificate(), url, pushDialog.branch().toStdString()));
// Push database
RemoteNetwork::get().push(path, url, remoteModel->currentClientCertificate(), pushDialog.name(),
pushDialog.commitMessage(), pushDialog.licence(), pushDialog.isPublic(), pushDialog.branch(),
pushDialog.forcePush(), commit_id);
}
void RemoteDock::newDirectoryNode(const QModelIndex& parent)
{
// Was this a new root dir?
if(!parent.isValid())
{
// Then check if there is a directory with the current user name
// Get current user name
QString user = RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoUser);
for(int i=0;i<remoteModel->rowCount();i++)
{
QModelIndex child = remoteModel->index(i, RemoteModelColumnName);
if(child.data().toString() == user)
{
ui->treeRemote->expand(child);
break;
}
}
}
}
void RemoteDock::reject()
{
// We override this, to ensure the Escape key doesn't make this dialog
// dock go away
return;
}
void RemoteDock::switchToMainView()
{
ui->stack->setCurrentIndex(0);
}
void RemoteDock::refreshLocalFileList()
{
remoteLocalFilesModel->refresh();
// Expand node for current user
QString user = RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoUser);
for(int i=0;i<remoteLocalFilesModel->rowCount();i++)
{
QModelIndex child = remoteLocalFilesModel->index(i, RemoteLocalFilesModel::ColumnName);
if(child.data().toString() == user)
{
ui->treeLocal->expand(child);
break;
}
}
}
void RemoteDock::openLocalFile(const QModelIndex& idx)
{
if(!idx.isValid())
return;
QString file = idx.sibling(idx.row(), RemoteLocalFilesModel::ColumnFile).data().toString();
if(!file.isEmpty())
emit openFile(Settings::getValue("remote", "clonedirectory").toString() + "/" + file);
}
void RemoteDock::fileOpened(const QString& filename)
{
// Clear data first
currently_opened_file_info.clear();
remoteCommitsModel->clear();
ui->comboDatabaseBranch->clear();
ui->editDatabaseUser->clear();
ui->editDatabaseFile->clear();
ui->editDatabaseBranch->clear();
// Do nothing if the file name is empty (indicating a closed database) or this is an in-memory database
if(filename.isEmpty() || filename == ":memory:")
return;
// Check if it is a tracked remote database file and retrieve the information we have on it
if(filename.startsWith(Settings::getValue("remote", "clonedirectory").toString()))
currently_opened_file_info = remoteDatabase.localGetLocalFileInfo(filename);
// Is this actually a clone of a remote database?
if(!currently_opened_file_info.file.empty())
{
// Copy information to view
ui->editDatabaseUser->setText(currently_opened_file_info.user_name());
ui->editDatabaseFile->setText(QString::fromStdString(currently_opened_file_info.name));
ui->editDatabaseBranch->setText(QString::fromStdString(currently_opened_file_info.branch));
// Make sure the current identity matches the identity used to clone this file in the first place.
// A mismatch is possible when the local database file has been opened using a recent files menu item or some similar technique.
if(QString::fromStdString(currently_opened_file_info.identity) != QFileInfo(remoteModel->currentClientCertificate()).fileName())
ui->comboUser->setCurrentIndex(ui->comboUser->findData("/" + QString::fromStdString(currently_opened_file_info.identity), Qt::UserRole, Qt::MatchEndsWith));
// Query more information on database from server
refreshMetadata(currently_opened_file_info.user_name(), QString::fromStdString(currently_opened_file_info.name));
// Switch to "Current Database" tab
ui->tabs->setCurrentIndex(2);
}
}
void RemoteDock::refreshMetadata(const QString& username, const QString& dbname)
{
// Make request for meta data
QUrl url(RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoServer) + "/metadata/get");
QUrlQuery query;
query.addQueryItem("username", username);
query.addQueryItem("folder", "/");
query.addQueryItem("dbname", dbname);
url.setQuery(query);
RemoteNetwork::get().fetch(url.toString(), RemoteNetwork::RequestTypeCustom, remoteModel->currentClientCertificate(), [this](const QByteArray& reply) {
// Read and check results
json obj = json::parse(reply, nullptr, false);
if(obj.is_discarded() || !obj.is_object())
return;
// Store all the commit information as-is
json obj_commits = obj["commits"];
current_commit_json = obj_commits.dump();
// Store the link to the web page in the action for opening that link in a browser
ui->actionDatabaseOpenBrowser->setData(QString::fromStdString(obj["web_page"]));
// Fill branches combo box
json obj_branches = obj["branches"];
ui->comboDatabaseBranch->clear();
for(auto it=obj_branches.cbegin();it!=obj_branches.cend();++it)
ui->comboDatabaseBranch->addItem(QString::fromStdString(it.key()), QString::fromStdString(it.value()["commit"]));
ui->comboDatabaseBranch->setCurrentIndex(ui->comboDatabaseBranch->findText(ui->editDatabaseBranch->text()));
});
}
void RemoteDock::deleteLocalDatabase(const QModelIndex& index)
{
if(!index.isValid())
return;
QString filename = index.sibling(index.row(), RemoteLocalFilesModel::ColumnFile).data().toString();
QString path = Settings::getValue("remote", "clonedirectory").toString() + "/" + filename;
// Warn when trying to delete a currently opened database file
if(mainWindow->getDb().currentFile() == path)
{
QMessageBox::warning(this, QApplication::applicationName(), tr("The database you are trying to delete is currently opened. "
"Please close it before deleting."));
return;
}
// Let user confirm deleting the database
if(QMessageBox::warning(this, QApplication::applicationName(), tr("This deletes the local version of this database with all the "
"changes you have not committed yet. Are you sure you want to "
"delete this database?"),
QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Cancel)
{
return;
}
// Delete the file
remoteLocalFilesModel->removeRow(index.row(), index.parent());
}
void RemoteDock::openCurrentDatabaseInBrowser() const
{
QDesktopServices::openUrl(ui->actionDatabaseOpenBrowser->data().toUrl());
}
void RemoteDock::refresh()
{
// Refresh Remote tab
remoteModel->refresh();
// Refresh Local tab
refreshLocalFileList();
// Refresh Current Database tab
if(!currently_opened_file_info.file.empty())
refreshMetadata(currently_opened_file_info.user_name(), QString::fromStdString(currently_opened_file_info.name));
}
void RemoteDock::pushFinished(const QString& filename, const QString& identity, const QUrl& url, const std::string& new_commit_id,
const std::string& branch, const QString& source_file)
{
// Create or update the record in our local checkout database
QString saveFileAs = remoteDatabase.localAdd(filename, identity, url, new_commit_id, branch);
// If the name of the source file and the name we're saving as differ, we're doing an initial push. In this case, copy the source file to
// the destination path to avoid redownloading it when it's first used.
if(saveFileAs != source_file)
QFile::copy(source_file, saveFileAs);
// Update info on currently opened file
if(currently_opened_file_info.file == QFileInfo(saveFileAs).fileName().toStdString())
currently_opened_file_info = remoteDatabase.localGetLocalFileInfo(saveFileAs);
// Refresh view
refresh();
}
void RemoteDock::fetchFinished(const QString& filename, const QString& identity, const QUrl& url, const std::string& new_commit_id,
const std::string& branch, const QDateTime& last_modified, QIODevice* device)
{
// Add cloned database to list of local databases
QString saveFileAs = remoteDatabase.localAdd(filename, identity, url, new_commit_id, branch);
// Save the downloaded data under the generated file name
QFile file(saveFileAs);
file.open(QIODevice::WriteOnly);
file.write(device->readAll());
// Set last modified data of the new file to the one provided by the server
// Before version 5.10, Qt didn't offer any option to set this attribute, so we're not setting it at the moment
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
file.setFileTime(last_modified, QFileDevice::FileModificationTime);
#endif
// Close file
file.close();
// Update info on currently opened file
currently_opened_file_info = remoteDatabase.localGetLocalFileInfo(saveFileAs);
// Refresh data
refreshLocalFileList();
// Tell the application to open this file
emit openFile(saveFileAs);
}
bool RemoteDock::isLocalDatabaseModified(const QString& local_file, const QString& username, const QString& dbname, const QString& identity, const std::string& commit_id)
{
// Fetch metadata on database
QUrl url(RemoteNetwork::get().getInfoFromClientCert(identity, RemoteNetwork::CertInfoServer) + "/metadata/get");
QUrlQuery query;
query.addQueryItem("username", username);
query.addQueryItem("folder", "/");
query.addQueryItem("dbname", dbname);
url.setQuery(query);
bool modified = true; // By default we assume the database has been modified
RemoteNetwork::get().fetch(url, RemoteNetwork::RequestTypeCustom, identity, [commit_id, dbname, local_file, &modified](const QByteArray& reply) {
// Read and check results
json obj = json::parse(reply, nullptr, false);
if(obj.is_discarded() || !obj.is_object())
return;
// Search tree entry for currently checked out commit
json tree = obj["commits"][commit_id]["tree"]["entries"];
for(const auto& it : tree)
{
if(it["entry_type"] == "db" && it["name"] == dbname.toStdString())
{
// When we have found it, check if the current file matches the remote file by first checking if the
// file size is equal and then comparing their SHA256 hashes
if(QFileInfo(local_file).size() == it["size"])
{
QFile file(local_file);
if(!file.open(QFile::ReadOnly))
return;
QCryptographicHash hash(QCryptographicHash::Sha256);
hash.addData(&file);
if(hash.result().toHex().toStdString() == it["sha256"])
{
modified = false;
return;
}
}
break;
}
}
}, true);
return modified;
}