Initial QSfera import
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
find_package(LibreGraphAPI 1.0.4 REQUIRED)
|
||||
set_package_properties(LibreGraphAPI PROPERTIES
|
||||
URL https://github.com/owncloud/libre-graph-api-cpp-qt-client.git
|
||||
DESCRIPTION "Libre Graph is a free API for cloud collaboration inspired by the MS Graph API"
|
||||
TYPE REQUIRED
|
||||
)
|
||||
|
||||
configure_file(config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
|
||||
|
||||
|
||||
add_library(libsync SHARED
|
||||
account.cpp
|
||||
bandwidthmanager.cpp
|
||||
capabilities.cpp
|
||||
cookiejar.cpp
|
||||
discovery.cpp
|
||||
discoveryinfo.cpp
|
||||
discoveryphase.cpp
|
||||
discoveryremoteinfo.cpp
|
||||
filesystem.cpp
|
||||
httplogger.cpp
|
||||
jobqueue.cpp
|
||||
logger.cpp
|
||||
accessmanager.cpp
|
||||
configfile.cpp
|
||||
globalconfig.cpp
|
||||
abstractnetworkjob.cpp
|
||||
networkjobs.cpp
|
||||
owncloudpropagator.cpp
|
||||
qsferatheme.cpp
|
||||
platform.cpp
|
||||
progressdispatcher.cpp
|
||||
propagatorjobs.cpp
|
||||
propagatedownload.cpp
|
||||
propagateupload.cpp
|
||||
propagateuploadv1.cpp
|
||||
propagateuploadtus.cpp
|
||||
propagateremotedelete.cpp
|
||||
propagateremotemove.cpp
|
||||
propagateremotemkdir.cpp
|
||||
syncengine.cpp
|
||||
syncfileitem.cpp
|
||||
syncfilestatustracker.cpp
|
||||
localdiscoverytracker.cpp
|
||||
syncresult.cpp
|
||||
syncoptions.cpp
|
||||
theme.cpp
|
||||
|
||||
creds/abstractcredentials.cpp
|
||||
creds/credentialmanager.cpp
|
||||
creds/httpcredentials.cpp
|
||||
creds/oauth.cpp
|
||||
creds/jwt.cpp
|
||||
creds/webfinger.cpp
|
||||
creds/idtoken.cpp
|
||||
|
||||
networkjobs/getfilejob.cpp
|
||||
networkjobs/checkserverjobfactory.cpp
|
||||
networkjobs/fetchuserinfojobfactory.cpp
|
||||
networkjobs/jsonjob.cpp
|
||||
networkjobs/simplenetworkjob.cpp
|
||||
networkjobs/resources.cpp
|
||||
|
||||
abstractcorejob.cpp
|
||||
|
||||
appprovider.cpp
|
||||
|
||||
path.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_sources(libsync PRIVATE platform_win.cpp)
|
||||
elseif(UNIX)
|
||||
target_sources(libsync PRIVATE xattr.cpp)
|
||||
if (APPLE)
|
||||
target_sources(libsync PRIVATE platform_mac.mm)
|
||||
else()
|
||||
target_sources(libsync PRIVATE platform_unix.cpp)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# These headers are installed for libqsferasync to be used by 3rd party apps
|
||||
INSTALL(
|
||||
FILES
|
||||
${CMAKE_CURRENT_BINARY_DIR}/qsferasynclib.h
|
||||
logger.h
|
||||
accessmanager.h
|
||||
DESTINATION ${KDE_INSTALL_INCLUDEDIR}/${APPLICATION_SHORTNAME}/libsync
|
||||
)
|
||||
|
||||
set_target_properties(libsync PROPERTIES EXPORT_NAME SyncCore)
|
||||
|
||||
target_link_libraries(libsync
|
||||
PUBLIC
|
||||
QSferaResources
|
||||
Qt::Core
|
||||
Qt::Network
|
||||
Qt::Widgets
|
||||
Qt::QuickControls2
|
||||
|
||||
PRIVATE
|
||||
Qt::Concurrent
|
||||
ZLIB::ZLIB
|
||||
Qt6Keychain::Qt6Keychain
|
||||
SQLite::SQLite3
|
||||
)
|
||||
|
||||
apply_common_target_settings(libsync)
|
||||
ecm_add_qml_module(libsync
|
||||
URI eu.QSfera.libsync
|
||||
VERSION 1.0
|
||||
NAMESPACE OCC
|
||||
GENERATE_PLUGIN_SOURCE
|
||||
)
|
||||
|
||||
ecm_finalize_qml_module(libsync DESTINATION ${KDE_INSTALL_QMLDIR})
|
||||
|
||||
set_source_files_properties(configfile.cpp
|
||||
PROPERTIES
|
||||
COMPILE_DEFINITIONS EXCLUDE_FILE_NAME="${EXCLUDE_FILE_NAME}"
|
||||
)
|
||||
|
||||
target_link_libraries(libsync PUBLIC $<BUILD_INTERFACE:OpenAPI::LibreGraphAPI>)
|
||||
add_subdirectory(graphapi)
|
||||
add_subdirectory(vfs)
|
||||
|
||||
if ( APPLE )
|
||||
target_link_libraries(libsync PUBLIC
|
||||
/System/Library/Frameworks/CoreServices.framework
|
||||
/System/Library/Frameworks/Foundation.framework
|
||||
/System/Library/Frameworks/AppKit.framework
|
||||
/System/Library/Frameworks/IOKit.framework
|
||||
)
|
||||
endif()
|
||||
|
||||
if(Inotify_FOUND)
|
||||
target_include_directories(libsync PRIVATE ${Inotify_INCLUDE_DIRS})
|
||||
target_link_libraries(libsync PUBLIC ${Inotify_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(NO_MSG_HANDLER)
|
||||
target_compile_definitions(libsync PRIVATE -DNO_MSG_HANDLER=1)
|
||||
endif()
|
||||
|
||||
GENERATE_EXPORT_HEADER(libsync
|
||||
EXPORT_MACRO_NAME QSFERA_SYNC_EXPORT
|
||||
EXPORT_FILE_NAME qsferasynclib.h
|
||||
STATIC_DEFINE QSFERA_BUILT_AS_STATIC
|
||||
)
|
||||
|
||||
set_target_properties(libsync PROPERTIES
|
||||
OUTPUT_NAME "QSferaLibSync"
|
||||
VERSION ${MIRALL_VERSION}
|
||||
SOVERSION ${MIRALL_SOVERSION}
|
||||
)
|
||||
|
||||
if(WITH_CRASHREPORTER)
|
||||
set_source_files_properties(platform.cpp PROPERTIES COMPILE_DEFINITIONS CRASHREPORTER_EXECUTABLE="${CRASHREPORTER_EXECUTABLE}")
|
||||
target_link_libraries(libsync PRIVATE CrashReporterQt::Handler)
|
||||
|
||||
if(UNIX AND NOT MAC)
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(libsync PUBLIC Threads::Threads)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_sources(libsync PRIVATE
|
||||
csync.cpp
|
||||
csync_exclude.cpp
|
||||
)
|
||||
|
||||
|
||||
try_compile(HAS_CLOCK_CAST
|
||||
SOURCE_FROM_CONTENT chrono_test.cpp "
|
||||
#include <chrono>
|
||||
int main() {
|
||||
std::chrono::clock_cast<std::chrono::system_clock>;
|
||||
}"
|
||||
)
|
||||
message(STATUS "HAS_CLOCK_CAST: ${HAS_CLOCK_CAST}")
|
||||
|
||||
if(HAS_CLOCK_CAST)
|
||||
set_source_files_properties(filesystem.cpp PROPERTIES COMPILE_DEFINITIONS HAS_CLOCK_CAST)
|
||||
endif()
|
||||
|
||||
add_subdirectory(common)
|
||||
|
||||
install(TARGETS libsync EXPORT ${APPLICATION_SHORTNAME}Config ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
* Copyright (C) Fabian Müller <fmueller@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "abstractcorejob.h"
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
AbstractCoreJobFactory::AbstractCoreJobFactory(QNetworkAccessManager *nam)
|
||||
: _nam(nam)
|
||||
{
|
||||
}
|
||||
|
||||
AbstractCoreJobFactory::~AbstractCoreJobFactory()
|
||||
{
|
||||
}
|
||||
|
||||
QNetworkAccessManager *AbstractCoreJobFactory::nam() const
|
||||
{
|
||||
return _nam;
|
||||
}
|
||||
|
||||
void AbstractCoreJobFactory::setJobResult(CoreJob *job, const QVariant &result)
|
||||
{
|
||||
job->setResult(result);
|
||||
}
|
||||
|
||||
void AbstractCoreJobFactory::setJobError(CoreJob *job, const QString &errorMessage)
|
||||
{
|
||||
job->setError(errorMessage);
|
||||
}
|
||||
|
||||
const QVariant &CoreJob::result() const
|
||||
{
|
||||
return _result;
|
||||
}
|
||||
|
||||
const QString &CoreJob::errorMessage() const
|
||||
{
|
||||
return _errorMessage;
|
||||
}
|
||||
|
||||
QNetworkReply *CoreJob::reply() const
|
||||
{
|
||||
return _reply;
|
||||
}
|
||||
|
||||
bool CoreJob::success() const
|
||||
{
|
||||
return _success;
|
||||
}
|
||||
|
||||
void CoreJob::setResult(const QVariant &result)
|
||||
{
|
||||
if (OC_ENSURE(assertNotFinished())) {
|
||||
_success = true;
|
||||
_result = result;
|
||||
|
||||
Q_EMIT finished();
|
||||
}
|
||||
}
|
||||
|
||||
void CoreJob::setError(const QString &errorMessage)
|
||||
{
|
||||
if (OC_ENSURE(assertNotFinished())) {
|
||||
_errorMessage = errorMessage;
|
||||
|
||||
Q_EMIT finished();
|
||||
}
|
||||
}
|
||||
|
||||
CoreJob::CoreJob(QNetworkReply *reply, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _reply(reply)
|
||||
{
|
||||
_reply->setParent(this);
|
||||
connect(this, &CoreJob::finished, this, &CoreJob::deleteLater, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
bool CoreJob::assertNotFinished() const
|
||||
{
|
||||
OC_ASSERT(_result.isNull());
|
||||
OC_ASSERT(_errorMessage.isEmpty());
|
||||
return _result.isNull() && _errorMessage.isEmpty();
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
* Copyright (C) Fabian Müller <fmueller@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "abstractnetworkjob.h"
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* This class manages an (HTTP) network job's result. It holds a result on success and error details on failures.
|
||||
* The class is universally usable for all kinds of network requests, there is no difference in handling the responses.
|
||||
* Instead, instances are created that start a suitable request and wire up the signals accordingly.
|
||||
* In contrast to the traditional network jobs (e.g., SimpleNetworkJob), core jobs are not bound to an account. Therefore,
|
||||
* they can be used with ease in situations where an account object is not available (e.g., the new wizard).
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT CoreJob : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class AbstractCoreJobFactory;
|
||||
|
||||
public:
|
||||
explicit CoreJob(QNetworkReply *reply, QObject *parent);
|
||||
|
||||
[[nodiscard]] const QVariant &result() const;
|
||||
|
||||
[[nodiscard]] const QString &errorMessage() const;
|
||||
|
||||
[[nodiscard]] QNetworkReply *reply() const;
|
||||
|
||||
[[nodiscard]] bool success() const;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Set job result. This emits the finished() signal, and marks the job as successful.
|
||||
* The job result or error can be set only once.
|
||||
* @param result job result
|
||||
*/
|
||||
void setResult(const QVariant &result);
|
||||
|
||||
/**
|
||||
* Set job error details. This emits the finished() signal, and marks the job as failed.
|
||||
* The job result or error can be set only once.
|
||||
* @param errorMessage network error or other suitable error message
|
||||
*/
|
||||
void setError(const QString &errorMessage);
|
||||
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* Emitted when a result or error is set. May be emitted once only.
|
||||
*/
|
||||
void finished();
|
||||
|
||||
/**
|
||||
* Emitted whenever the user accepts a CA certificate which is not trusted yet using the TlsErrorDialog.
|
||||
* Handlers should store the certificate in the access manager passed to the corresponding job.
|
||||
* @param caCertificate accepted CA certificate
|
||||
*/
|
||||
void caCertificateAccepted(const QSslCertificate &caCertificate);
|
||||
|
||||
private:
|
||||
// job result/error should be set only once, because that emits the "finished" signal
|
||||
[[nodiscard]] bool assertNotFinished() const;
|
||||
|
||||
bool _success = false;
|
||||
|
||||
QVariant _result;
|
||||
|
||||
QString _errorMessage;
|
||||
QNetworkReply *_reply = nullptr;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Abstract base class for core job factories.
|
||||
*
|
||||
* Jobs are built by the startJob factory method, which creates a job instance as well as a network request, wires the required signals up, then sends the request.
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT AbstractCoreJobFactory
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Create a new instance
|
||||
* @param nam network access manager used to send the requests
|
||||
* @param parent optional parent which will be set at this object's parent as well as all jobs' parent.
|
||||
*/
|
||||
explicit AbstractCoreJobFactory(QNetworkAccessManager *nam);
|
||||
virtual ~AbstractCoreJobFactory();
|
||||
|
||||
/**
|
||||
* Send network request and return associated job.
|
||||
* @param url URL to send request to
|
||||
* @return job
|
||||
*/
|
||||
virtual CoreJob *startJob(const QUrl &url, QObject *parent) = 0;
|
||||
|
||||
protected:
|
||||
[[nodiscard]] QNetworkAccessManager *nam() const;
|
||||
|
||||
/**
|
||||
* Set job result. Needed because the jobs' methods are protected, and this class is a friend of Job.
|
||||
* The job result or error can be set only once.
|
||||
* @param result job result
|
||||
*/
|
||||
static void setJobResult(CoreJob *job, const QVariant &result);
|
||||
|
||||
/**
|
||||
* Set job error details. Needed because the jobs' methods are protected, and this class is a friend of Job.
|
||||
* @param errorMessage network error or other suitable error message
|
||||
*/
|
||||
static void setJobError(CoreJob *job, const QString &errorMessage);
|
||||
|
||||
/**
|
||||
* Factory to create QNetworkRequests with properly set timeout.
|
||||
*/
|
||||
template <typename... Params>
|
||||
[[nodiscard]] static QNetworkRequest makeRequest(Params... params)
|
||||
{
|
||||
auto request = QNetworkRequest(params...);
|
||||
|
||||
const auto timeoutMilliseconds = static_cast<int>(AbstractNetworkJob::httpTimeout.count());
|
||||
request.setTransferTimeout(timeoutMilliseconds);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private:
|
||||
QNetworkAccessManager *_nam;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include <QAuthenticator>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "common/asserts.h"
|
||||
#include "networkjobs.h"
|
||||
#include "account.h"
|
||||
#include "owncloudpropagator.h"
|
||||
#include "httplogger.h"
|
||||
|
||||
#include "creds/abstractcredentials.h"
|
||||
|
||||
Q_DECLARE_METATYPE(QTimer *)
|
||||
|
||||
using namespace std::chrono;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
constexpr int MaxRetryCount = 5;
|
||||
}
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcNetworkJob, "sync.networkjob", QtInfoMsg)
|
||||
|
||||
// If not set, it is overwritten by the Application constructor with the value from the config
|
||||
milliseconds AbstractNetworkJob::httpTimeout = AbstractNetworkJob::DefaultHttpTimeout;
|
||||
|
||||
AbstractNetworkJob::AbstractNetworkJob(AccountPtr account, const QUrl &baseUrl, const QString &path, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _account(account)
|
||||
, _baseUrl(baseUrl)
|
||||
, _path(path)
|
||||
{
|
||||
// Since we hold a QSharedPointer to the account, this makes no sense. (issue #6893)
|
||||
Q_ASSERT(account != parent);
|
||||
Q_ASSERT(baseUrl.isValid());
|
||||
}
|
||||
|
||||
QUrl AbstractNetworkJob::baseUrl() const
|
||||
{
|
||||
return _baseUrl;
|
||||
}
|
||||
|
||||
QUrl AbstractNetworkJob::url() const
|
||||
{
|
||||
return Utility::concatUrlPath(baseUrl(), path(), query());
|
||||
}
|
||||
|
||||
|
||||
void AbstractNetworkJob::setQuery(const QUrlQuery &query)
|
||||
{
|
||||
_query = query;
|
||||
}
|
||||
|
||||
QUrlQuery AbstractNetworkJob::query() const
|
||||
{
|
||||
return _query;
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::setTimeout(const std::chrono::milliseconds sec)
|
||||
{
|
||||
_timeout = sec;
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::setForceIgnoreCredentialFailure(bool ignore)
|
||||
{
|
||||
_forceIgnoreCredentialFailure = ignore;
|
||||
}
|
||||
|
||||
bool AbstractNetworkJob::ignoreCredentialFailure() const
|
||||
{
|
||||
return _forceIgnoreCredentialFailure || _isAuthenticationJob;
|
||||
}
|
||||
|
||||
QNetworkReply *AbstractNetworkJob::reply() const
|
||||
{
|
||||
Q_ASSERT(_reply);
|
||||
return _reply;
|
||||
}
|
||||
|
||||
bool AbstractNetworkJob::isAuthenticationJob() const
|
||||
{
|
||||
return _isAuthenticationJob;
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::setAuthenticationJob(bool b)
|
||||
{
|
||||
_isAuthenticationJob = b;
|
||||
}
|
||||
|
||||
bool AbstractNetworkJob::needsRetry() const
|
||||
{
|
||||
if (isAuthenticationJob()) {
|
||||
qCDebug(lcNetworkJob) << u"Not Retry auth job" << this << url();
|
||||
return false;
|
||||
}
|
||||
if (retryCount() >= MaxRetryCount) {
|
||||
qCDebug(lcNetworkJob) << u"Not Retry too many retries" << retryCount() << this << url();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (auto reply = this->reply()) {
|
||||
// we had an unsupported redirect
|
||||
if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull()) {
|
||||
return true;
|
||||
}
|
||||
switch (reply->error()) {
|
||||
case QNetworkReply::AuthenticationRequiredError:
|
||||
return true;
|
||||
case QNetworkReply::ContentReSendError:
|
||||
if (_reply->attribute(QNetworkRequest::Http2WasUsedAttribute).toBool()) {
|
||||
return true;
|
||||
}
|
||||
[[fallthrough]];
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::sendRequest(const QByteArray &verb, const QNetworkRequest &req, std::unique_ptr<QIODevice> &&requestBody)
|
||||
{
|
||||
_verb = verb;
|
||||
|
||||
_request = req;
|
||||
|
||||
// allow transferring files with a big compression ratio
|
||||
_request.setDecompressedSafetyCheckThreshold(-1);
|
||||
|
||||
_request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, _storeInCache);
|
||||
|
||||
if (_cacheLoadControl.has_value()) {
|
||||
_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, _cacheLoadControl.value());
|
||||
}
|
||||
|
||||
if (requestBody) {
|
||||
_requestBody = requestBody.release();
|
||||
_requestBody->setParent(this);
|
||||
}
|
||||
|
||||
Q_ASSERT(_request.url().isEmpty() || _request.url() == url());
|
||||
Q_ASSERT(_request.transferTimeout() == 0 || _request.transferTimeout() == _timeout.count());
|
||||
|
||||
_request.setUrl(url());
|
||||
_request.setPriority(_priority);
|
||||
_request.setTransferTimeout(_timeout);
|
||||
|
||||
if (!isAuthenticationJob() && _account->jobQueue()->enqueue(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto reply = _account->sendRawRequest(verb, _request.url(), _request, _requestBody);
|
||||
adoptRequest(reply);
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::adoptRequest(QPointer<QNetworkReply> reply)
|
||||
{
|
||||
std::swap(_reply, reply);
|
||||
if (reply) {
|
||||
reply->disconnect();
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
_request = _reply->request();
|
||||
|
||||
connect(_reply, &QNetworkReply::finished, this, &AbstractNetworkJob::slotFinished);
|
||||
|
||||
newReplyHook(_reply);
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::slotFinished()
|
||||
{
|
||||
_finished = true;
|
||||
|
||||
_account->credentials()->checkCredentials(_reply);
|
||||
|
||||
if (_reply->error() != QNetworkReply::NoError) {
|
||||
if (_account->jobQueue()->retry(this)) {
|
||||
qCDebug(lcNetworkJob) << u"Queued:" << this << u"for retry";
|
||||
return;
|
||||
}
|
||||
|
||||
if (_reply->error() == QNetworkReply::OperationCanceledError && !_aborted) {
|
||||
_timedout = true;
|
||||
}
|
||||
Q_EMIT networkError(_reply);
|
||||
}
|
||||
|
||||
// get the Date timestamp from reply
|
||||
_responseTimestamp = _reply->rawHeader("Date");
|
||||
|
||||
if (!reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull() && !(isAuthenticationJob() || reply()->request().hasRawHeader(QByteArrayLiteral("OC-Connection-Validator")))) {
|
||||
Q_EMIT _account->unknownConnectionState();
|
||||
qCWarning(lcNetworkJob) << this << u"Unsupported redirect on" << _reply->url().toString() << u"to"
|
||||
<< reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).toString();
|
||||
Q_EMIT networkError(_reply);
|
||||
if (_account->jobQueue()->retry(this)) {
|
||||
qCWarning(lcNetworkJob) << u"Retry Nr:" << _retryCount << _reply->url();
|
||||
return;
|
||||
} else {
|
||||
qCWarning(lcNetworkJob) << u"Don't retry:" << _reply->url();
|
||||
}
|
||||
}
|
||||
Q_EMIT aboutToFinishSignal(AbstractNetworkJob::QPrivateSignal());
|
||||
finished();
|
||||
Q_EMIT finishedSignal(AbstractNetworkJob::QPrivateSignal());
|
||||
qCDebug(lcNetworkJob) << u"Network job finished" << this;
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
QByteArray AbstractNetworkJob::responseTimestamp() const
|
||||
{
|
||||
return _responseTimestamp;
|
||||
}
|
||||
|
||||
QDateTime OCC::AbstractNetworkJob::responseQTimeStamp() const
|
||||
{
|
||||
return Utility::parseRFC1123Date(QString::fromUtf8(responseTimestamp()));
|
||||
}
|
||||
|
||||
QByteArray AbstractNetworkJob::requestId()
|
||||
{
|
||||
return _reply ? _reply->request().rawHeader("X-Request-ID") : QByteArray();
|
||||
}
|
||||
|
||||
QString AbstractNetworkJob::errorString() const
|
||||
{
|
||||
if (_timedout) {
|
||||
return tr("Connection timed out");
|
||||
} else if (!reply()) {
|
||||
return tr("Unknown error: network reply was deleted");
|
||||
} else if (reply()->hasRawHeader("OC-ErrorString")) {
|
||||
return QString::fromUtf8(reply()->rawHeader("OC-ErrorString"));
|
||||
} else {
|
||||
return networkReplyErrorString(*reply());
|
||||
}
|
||||
}
|
||||
|
||||
QString AbstractNetworkJob::errorStringParsingBody(QByteArray *body)
|
||||
{
|
||||
QString base = errorString();
|
||||
if (base.isEmpty() || !reply()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QByteArray replyBody = reply()->readAll();
|
||||
if (body) {
|
||||
*body = replyBody;
|
||||
}
|
||||
|
||||
QString extra = extractErrorMessage(replyBody);
|
||||
// Don't append the XML error message to a OC-ErrorString message.
|
||||
if (!extra.isEmpty() && !reply()->hasRawHeader("OC-ErrorString")) {
|
||||
return QStringLiteral("%1 (%2)").arg(base, extra);
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
AbstractNetworkJob::~AbstractNetworkJob()
|
||||
{
|
||||
if (!_finished && !_aborted && !_timedout) {
|
||||
qCCritical(lcNetworkJob) << u"Deleting running job" << this;
|
||||
}
|
||||
if (_reply) {
|
||||
// the body must live as long as the reply exists
|
||||
// until now the body was parented by this network job
|
||||
if (_requestBody) {
|
||||
_requestBody->setParent(_reply);
|
||||
}
|
||||
_reply->disconnect(this);
|
||||
_reply->abort();
|
||||
_reply->deleteLater();
|
||||
_reply.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::start()
|
||||
{
|
||||
qCInfo(lcNetworkJob) << u"Created" << this << u"for" << parent();
|
||||
}
|
||||
|
||||
QString AbstractNetworkJob::replyStatusString() {
|
||||
Q_ASSERT(reply());
|
||||
if (reply()->error() == QNetworkReply::NoError) {
|
||||
return QStringLiteral("OK");
|
||||
} else {
|
||||
return QStringLiteral("%1, %2").arg(Utility::enumToString(reply()->error()), errorString());
|
||||
}
|
||||
}
|
||||
|
||||
QString extractErrorMessage(const QByteArray &errorResponse)
|
||||
{
|
||||
QXmlStreamReader reader(errorResponse);
|
||||
reader.readNextStartElement();
|
||||
if (reader.name() != QLatin1String("error")) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString exception;
|
||||
while (!reader.atEnd() && !reader.hasError()) {
|
||||
reader.readNextStartElement();
|
||||
if (reader.name() == QLatin1String("message")) {
|
||||
QString message = reader.readElementText();
|
||||
if (!message.isEmpty()) {
|
||||
return message;
|
||||
}
|
||||
} else if (reader.name() == QLatin1String("exception")) {
|
||||
exception = reader.readElementText();
|
||||
}
|
||||
}
|
||||
// Fallback, if message could not be found
|
||||
return exception;
|
||||
}
|
||||
|
||||
QString errorMessage(const QString &baseError, const QByteArray &body)
|
||||
{
|
||||
QString msg = baseError;
|
||||
QString extra = extractErrorMessage(body);
|
||||
if (!extra.isEmpty()) {
|
||||
msg += QStringLiteral(" (%1)").arg(extra);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
QString networkReplyErrorString(const QNetworkReply &reply)
|
||||
{
|
||||
QString base = reply.errorString();
|
||||
int httpStatus = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
QString httpReason = reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
|
||||
|
||||
// Only adjust HTTP error messages of the expected format.
|
||||
if (httpReason.isEmpty() || httpStatus == 0 || !base.contains(httpReason)) {
|
||||
return base;
|
||||
}
|
||||
|
||||
return AbstractNetworkJob::tr("Server replied \"%1 %2\" to \"%3 %4\"").arg(QString::number(httpStatus), httpReason, QString::fromLatin1(HttpLogger::requestVerb(reply)), reply.request().url().toDisplayString());
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::retry()
|
||||
{
|
||||
OC_ENFORCE(!_verb.isEmpty());
|
||||
_retryCount++;
|
||||
qCInfo(lcNetworkJob) << u"Restarting" << this << u"for the" << _retryCount << u"time";
|
||||
if (_requestBody) {
|
||||
if (!_requestBody->isSequential()) {
|
||||
Q_ASSERT(_requestBody->isOpen());
|
||||
_requestBody->seek(0);
|
||||
} else {
|
||||
qCWarning(lcNetworkJob) << u"Can't resend request, body not suitable" << this;
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// this will reuse _requestBody
|
||||
sendRequest(_verb, _request);
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::abort()
|
||||
{
|
||||
if (_reply) {
|
||||
// calling abort will trigger the execution of finished()
|
||||
// with _reply->error() == QNetworkReply::OperationCanceledError
|
||||
// the api user can then decide whether to discard this job or retry it.
|
||||
// The order is important, mark as _aborted before we abort the reply which will trigger slotFinished
|
||||
_aborted = true;
|
||||
_reply->abort();
|
||||
} else {
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::setPriority(QNetworkRequest::Priority priority)
|
||||
{
|
||||
_priority = priority;
|
||||
}
|
||||
|
||||
QNetworkRequest::Priority AbstractNetworkJob::priority() const
|
||||
{
|
||||
return _priority;
|
||||
}
|
||||
|
||||
int AbstractNetworkJob::httpStatusCode() const
|
||||
{
|
||||
return reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::setStoreInCache(bool storeInCache)
|
||||
{
|
||||
_storeInCache = storeInCache;
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::setCacheLoadControl(QNetworkRequest::CacheLoadControl cacheLoadControl)
|
||||
{
|
||||
_cacheLoadControl = cacheLoadControl;
|
||||
}
|
||||
|
||||
QPointer<QIODevice> AbstractNetworkJob::body() const
|
||||
{
|
||||
return _requestBody;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
QDebug operator<<(QDebug debug, const OCC::AbstractNetworkJob *job)
|
||||
{
|
||||
QDebugStateSaver saver(debug);
|
||||
debug.setAutoInsertSpaces(false);
|
||||
debug << job->metaObject()->className() << u"(" << job->account().data() << u", " << job->url().toDisplayString() << u", " << job->_verb;
|
||||
if (auto reply = job->_reply) {
|
||||
debug << u", Original-Request-ID: " << reply->request().rawHeader("Original-Request-ID") << u", X-Request-ID: "
|
||||
<< reply->request().rawHeader("X-Request-ID");
|
||||
|
||||
const auto errorString = reply->rawHeader(QByteArrayLiteral("OC-ErrorString"));
|
||||
if (!errorString.isEmpty()) {
|
||||
debug << u", Error:" << errorString;
|
||||
}
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
debug << u", NetworkError: " << reply->errorString();
|
||||
}
|
||||
}
|
||||
if (job->_timedout) {
|
||||
debug << u", timedout";
|
||||
}
|
||||
debug << u")";
|
||||
return debug.maybeSpace();
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "accountfwd.h"
|
||||
#include "jobqueue.h"
|
||||
|
||||
#include "common/asserts.h"
|
||||
|
||||
#include "qsferasynclib.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QElapsedTimer>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
|
||||
class QUrl;
|
||||
|
||||
|
||||
QSFERA_SYNC_EXPORT QDebug operator<<(QDebug debug, const OCC::AbstractNetworkJob *job);
|
||||
|
||||
namespace OCC {
|
||||
|
||||
using HeaderMap = QMap<QByteArray, QByteArray>;
|
||||
|
||||
/**
|
||||
* @brief The AbstractNetworkJob class
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT AbstractNetworkJob : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AbstractNetworkJob(AccountPtr account, const QUrl &baseUrl, const QString &path, QObject *parent = nullptr);
|
||||
~AbstractNetworkJob() override;
|
||||
|
||||
virtual void start();
|
||||
|
||||
AccountPtr account() const { return _account; }
|
||||
QString path() const { return _path; }
|
||||
|
||||
/*
|
||||
* A base Url, for most of the jobs this will be the WebDAV entry point.
|
||||
*/
|
||||
QUrl baseUrl() const;
|
||||
|
||||
/*
|
||||
* The absolute url: baseUrl() + path() + query()
|
||||
*/
|
||||
QUrl url() const;
|
||||
|
||||
QNetworkReply *reply() const;
|
||||
|
||||
void setForceIgnoreCredentialFailure(bool ignore);
|
||||
bool ignoreCredentialFailure() const;
|
||||
|
||||
QByteArray responseTimestamp() const;
|
||||
QDateTime responseQTimeStamp() const;
|
||||
|
||||
int httpStatusCode() const;
|
||||
|
||||
/* Content of the X-Request-ID header. (Only set after the request is sent) */
|
||||
QByteArray requestId();
|
||||
|
||||
auto timeoutSec() const { return _timeout; }
|
||||
bool timedOut() const { return _timedout; }
|
||||
bool aborted() const { return _aborted; }
|
||||
|
||||
void setPriority(QNetworkRequest::Priority priority);
|
||||
QNetworkRequest::Priority priority() const;
|
||||
|
||||
/** Returns an error message, if any. */
|
||||
QString errorString() const;
|
||||
|
||||
/** Like errorString, but also checking the reply body for information.
|
||||
*
|
||||
* Specifically, sometimes xml bodies have extra error information.
|
||||
* This function reads the body of the reply and parses out the
|
||||
* error information, if possible.
|
||||
*
|
||||
* \a body is optinally filled with the reply body.
|
||||
*
|
||||
* Warning: Needs to call reply()->readAll().
|
||||
*/
|
||||
QString errorStringParsingBody(QByteArray *body = nullptr);
|
||||
|
||||
/** Make a new request */
|
||||
void retry();
|
||||
|
||||
/** Abort the job due to an error */
|
||||
void abort();
|
||||
|
||||
/** static variable the HTTP timeout. If set to 0, the default will be used
|
||||
*/
|
||||
static std::chrono::milliseconds httpTimeout;
|
||||
|
||||
/**
|
||||
* The default 5 minutes timeout if none is specified by the config.
|
||||
* Qt's default would be 30s.
|
||||
*/
|
||||
static constexpr std::chrono::seconds DefaultHttpTimeout{std::chrono::minutes(5)};
|
||||
|
||||
/** whether or noth this job should be restarted after authentication */
|
||||
bool isAuthenticationJob() const;
|
||||
void setAuthenticationJob(bool b);
|
||||
|
||||
/** How many times was that job retried */
|
||||
int retryCount() const { return _retryCount; }
|
||||
|
||||
|
||||
virtual bool needsRetry() const;
|
||||
|
||||
void setTimeout(const std::chrono::milliseconds sec);
|
||||
|
||||
/**
|
||||
* Configure whether to store replies in a cache configured in the corresponding network access manager (if one is set there).
|
||||
* By default, replies are not stored in the cache. Qt normally would store all eligible resources (as configured by server policies) in the cache.
|
||||
* See also \ref setCacheLoadControl
|
||||
* @param storeInCache whether to store replies in the cache
|
||||
*/
|
||||
void setStoreInCache(bool storeInCache);
|
||||
|
||||
/**
|
||||
* Configure whether to load data from the cache.
|
||||
* We inherit Qt's default behavior (which in Qt 5.15 and 6.4 is to prefer a network response but provide a cached value if the server permits it in its
|
||||
* cache policies when the server cannot be reached).
|
||||
* See also \ref setStoreInCache
|
||||
* @param cacheLoadControl a policy suitable for the current job
|
||||
*/
|
||||
void setCacheLoadControl(QNetworkRequest::CacheLoadControl cacheLoadControl);
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the body of the the request, if set
|
||||
*/
|
||||
QPointer<QIODevice> body() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
/** Emitted on network error.
|
||||
*
|
||||
* \a reply is never null
|
||||
*/
|
||||
void networkError(QNetworkReply *reply);
|
||||
|
||||
/**
|
||||
* The job is done
|
||||
*/
|
||||
void aboutToFinishSignal(QPrivateSignal);
|
||||
void finishedSignal(QPrivateSignal);
|
||||
|
||||
protected:
|
||||
/** Initiate a network request, returning a QNetworkReply.
|
||||
*
|
||||
* Calls setReply() and setupConnections() on it.
|
||||
*
|
||||
* Takes ownership of the requestBody (to allow redirects).
|
||||
*/
|
||||
void sendRequest(const QByteArray &verb, const QNetworkRequest &req = QNetworkRequest(), std::unique_ptr<QIODevice> &&requestBody = {});
|
||||
|
||||
/** Can be used by derived classes to set up the network reply.
|
||||
*
|
||||
* Particularly useful when the request is redirected and reply()
|
||||
* changes. For things like setting up additional signal connections
|
||||
* on the new reply.
|
||||
*/
|
||||
virtual void newReplyHook(QNetworkReply *) {}
|
||||
|
||||
/** Called at the end of QNetworkReply::finished processing.
|
||||
*/
|
||||
virtual void finished() = 0;
|
||||
|
||||
QByteArray _responseTimestamp;
|
||||
|
||||
QString replyStatusString();
|
||||
|
||||
/*
|
||||
* The url query appended to the url.
|
||||
* The query will not be set as part of the body.
|
||||
* The query must be fully encoded.
|
||||
*/
|
||||
void setQuery(const QUrlQuery &query);
|
||||
QUrlQuery query() const;
|
||||
|
||||
AccountPtr _account;
|
||||
|
||||
private:
|
||||
/** Makes this job drive a pre-made QNetworkReply
|
||||
*
|
||||
* This reply cannot have a QIODevice request body because we can't get
|
||||
* at it and thus not resend it in case of redirects.
|
||||
*/
|
||||
void adoptRequest(QPointer<QNetworkReply> reply);
|
||||
void slotFinished();
|
||||
|
||||
const QUrl _baseUrl;
|
||||
const QString _path;
|
||||
|
||||
QUrlQuery _query;
|
||||
|
||||
std::chrono::milliseconds _timeout = httpTimeout;
|
||||
bool _timedout = false; // set to true when the timeout slot is received
|
||||
bool _aborted = false;
|
||||
bool _finished = false;
|
||||
bool _forceIgnoreCredentialFailure = false;
|
||||
|
||||
QNetworkRequest _request;
|
||||
QByteArray _verb;
|
||||
QPointer<QNetworkReply> _reply; // (QPointer because the NetworkManager may be destroyed before the jobs at exit)
|
||||
|
||||
// Set by the xyzRequest() functions and needed to be able to redirect
|
||||
// requests, should it be required.
|
||||
//
|
||||
// Reparented to the currently running QNetworkReply.
|
||||
QPointer<QIODevice> _requestBody;
|
||||
|
||||
bool _isAuthenticationJob = false;
|
||||
int _retryCount = 0;
|
||||
|
||||
// by default, we don't intend to store responses in the cache (if one is set in the account's access manager)
|
||||
bool _storeInCache = false;
|
||||
// we use Qt's default cache load behavior unless the user explicitly requests a different behavior
|
||||
std::optional<QNetworkRequest::CacheLoadControl> _cacheLoadControl = std::nullopt;
|
||||
|
||||
QNetworkRequest::Priority _priority = QNetworkRequest::NormalPriority;
|
||||
|
||||
friend QDebug(::operator<<)(QDebug debug, const AbstractNetworkJob *job);
|
||||
};
|
||||
|
||||
|
||||
/** Gets the SabreDAV-style error message from an error response.
|
||||
*
|
||||
* This assumes the response is XML with a 'error' tag that has a
|
||||
* 'message' tag that contains the data to extract.
|
||||
*
|
||||
* Returns a null string if no message was found.
|
||||
*/
|
||||
QString QSFERA_SYNC_EXPORT extractErrorMessage(const QByteArray &errorResponse);
|
||||
|
||||
/** Builds a error message based on the error and the reply body. */
|
||||
QString QSFERA_SYNC_EXPORT errorMessage(const QString &baseError, const QByteArray &body);
|
||||
|
||||
/** Nicer errorString() for QNetworkReply
|
||||
*
|
||||
* By default QNetworkReply::errorString() often produces messages like
|
||||
* "Error downloading <url> - server replied: <reason>"
|
||||
* but the "downloading" part invariably confuses people since the
|
||||
* error might very well have been produced by a PUT request.
|
||||
*
|
||||
* This function produces clearer error messages for HTTP errors.
|
||||
*/
|
||||
QString QSFERA_SYNC_EXPORT networkReplyErrorString(const QNetworkReply &reply);
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include <QAuthenticator>
|
||||
#include <QNetworkCookie>
|
||||
#include <QUuid>
|
||||
|
||||
#include "accessmanager.h"
|
||||
#include "common/utility.h"
|
||||
#include "cookiejar.h"
|
||||
#include "httplogger.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcAccessManager, "sync.accessmanager", QtInfoMsg)
|
||||
|
||||
AccessManager::AccessManager(QObject *parent)
|
||||
: QNetworkAccessManager(parent)
|
||||
{
|
||||
setCookieJar(new CookieJar);
|
||||
|
||||
connect(this, &AccessManager::sslErrors, this, [this](QNetworkReply *reply, const QList<QSslError> &errors) {
|
||||
auto filtered = errors;
|
||||
filtered.erase(std::remove_if(
|
||||
filtered.begin(), filtered.end(), [this](const QSslError &e) {
|
||||
return !_customTrustedCaCertificates.contains(e.certificate());
|
||||
}),
|
||||
filtered.end());
|
||||
reply->ignoreSslErrors(filtered);
|
||||
});
|
||||
}
|
||||
|
||||
QByteArray AccessManager::generateRequestId()
|
||||
{
|
||||
return QUuid::createUuid().toByteArray(QUuid::WithoutBraces);
|
||||
}
|
||||
|
||||
QNetworkReply *AccessManager::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData)
|
||||
{
|
||||
QNetworkRequest newRequest(request);
|
||||
newRequest.setRawHeader(QByteArrayLiteral("User-Agent"), Utility::userAgentString());
|
||||
|
||||
// Some firewalls reject requests that have a "User-Agent" but no "Accept" header
|
||||
newRequest.setRawHeader(QByteArrayLiteral("Accept"), QByteArrayLiteral("*/*"));
|
||||
|
||||
// Set the language, so messages from the server are localised correctly.
|
||||
newRequest.setRawHeader("Accept-Language", QLocale().name().toUtf8());
|
||||
|
||||
// we don't follow redirects, if we receive one the ConnectionValidor is triggered
|
||||
// -> default to manual redirection
|
||||
if (newRequest.attribute(QNetworkRequest::RedirectPolicyAttribute).isNull()) {
|
||||
newRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
|
||||
}
|
||||
|
||||
QByteArray verb = newRequest.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray();
|
||||
// For PROPFIND (assumed to be a WebDAV op), set xml/utf8 as content type/encoding
|
||||
// This needs extension
|
||||
if (verb == QByteArrayLiteral("PROPFIND")) {
|
||||
newRequest.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("text/xml; charset=utf-8"));
|
||||
}
|
||||
|
||||
// Generate a new request id
|
||||
const QByteArray requestId = generateRequestId();
|
||||
newRequest.setRawHeader(QByteArrayLiteral("X-Request-ID"), requestId);
|
||||
const auto originalIdKey = QByteArrayLiteral("Original-Request-ID");
|
||||
if (!newRequest.hasRawHeader(originalIdKey)) {
|
||||
newRequest.setRawHeader(originalIdKey, requestId);
|
||||
}
|
||||
|
||||
|
||||
static const bool http2DisabledEnv = qEnvironmentVariableIntValue("QSFERA_HTTP2_DISABLED") == 1;
|
||||
if (http2DisabledEnv) {
|
||||
newRequest.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
|
||||
}
|
||||
|
||||
// allow http pipelining
|
||||
newRequest.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
|
||||
|
||||
auto sslConfiguration = newRequest.sslConfiguration();
|
||||
|
||||
sslConfiguration.setSslOption(QSsl::SslOptionDisableSessionTickets, false);
|
||||
sslConfiguration.setSslOption(QSsl::SslOptionDisableSessionSharing, false);
|
||||
sslConfiguration.setSslOption(QSsl::SslOptionDisableSessionPersistence, false);
|
||||
if (!_customTrustedCaCertificates.isEmpty()) {
|
||||
// for some reason, passing an empty list causes the default chain to be removed
|
||||
// this behavior does not match the documentation
|
||||
sslConfiguration.addCaCertificates({ _customTrustedCaCertificates.begin(), _customTrustedCaCertificates.end() });
|
||||
}
|
||||
newRequest.setSslConfiguration(sslConfiguration);
|
||||
|
||||
const auto reply = QNetworkAccessManager::createRequest(op, newRequest, outgoingData);
|
||||
HttpLogger::logRequest(reply, op, outgoingData);
|
||||
return reply;
|
||||
}
|
||||
|
||||
QSet<QSslCertificate> AccessManager::customTrustedCaCertificates()
|
||||
{
|
||||
return _customTrustedCaCertificates;
|
||||
}
|
||||
|
||||
void AccessManager::setCustomTrustedCaCertificates(const QSet<QSslCertificate> &certificates)
|
||||
{
|
||||
_customTrustedCaCertificates = certificates;
|
||||
// we have to terminate the existing (cached) connection to make the access manager re-evaluate the certificate sent by the server
|
||||
clearConnectionCache();
|
||||
}
|
||||
|
||||
void AccessManager::addCustomTrustedCaCertificates(const QList<QSslCertificate> &certificates)
|
||||
{
|
||||
_customTrustedCaCertificates.unite({ certificates.begin(), certificates.end() });
|
||||
|
||||
// we have to terminate the existing (cached) connection to make the access manager re-evaluate the certificate sent by the server
|
||||
clearConnectionCache();
|
||||
}
|
||||
|
||||
CookieJar *AccessManager::openCloudCookieJar() const
|
||||
{
|
||||
auto jar = qobject_cast<CookieJar *>(cookieJar());
|
||||
Q_ASSERT(jar);
|
||||
return jar;
|
||||
}
|
||||
|
||||
QList<QSslError> AccessManager::filterSslErrors(const QList<QSslError> &errors) const
|
||||
{
|
||||
auto filtered = errors;
|
||||
filtered.erase(std::remove_if(filtered.begin(), filtered.end(),
|
||||
[this](const QSslError &e) { return e.certificate().isNull() || _customTrustedCaCertificates.contains(e.certificate()); }),
|
||||
filtered.end());
|
||||
return filtered;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#ifndef MIRALL_ACCESS_MANAGER_H
|
||||
#define MIRALL_ACCESS_MANAGER_H
|
||||
|
||||
#include "qsferasynclib.h"
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
class QByteArray;
|
||||
class QUrl;
|
||||
|
||||
namespace OCC {
|
||||
class CookieJar;
|
||||
|
||||
/**
|
||||
* @brief The AccessManager class
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT AccessManager : public QNetworkAccessManager
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static QByteArray generateRequestId();
|
||||
|
||||
AccessManager(QObject *parent = nullptr);
|
||||
|
||||
QSet<QSslCertificate> customTrustedCaCertificates();
|
||||
|
||||
/***
|
||||
* Warning calling those will break running network jobs
|
||||
*/
|
||||
void setCustomTrustedCaCertificates(const QSet<QSslCertificate> &certificates);
|
||||
/***
|
||||
* Warning calling those will break running network jobs
|
||||
*/
|
||||
void addCustomTrustedCaCertificates(const QList<QSslCertificate> &certificates);
|
||||
|
||||
CookieJar *openCloudCookieJar() const;
|
||||
|
||||
/***
|
||||
* Remove all errors for already accepted certificates
|
||||
*/
|
||||
QList<QSslError> filterSslErrors(const QList<QSslError> &errors) const;
|
||||
|
||||
protected:
|
||||
QNetworkReply *createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr) override;
|
||||
|
||||
private:
|
||||
QSet<QSslCertificate> _customTrustedCaCertificates;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,377 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "account.h"
|
||||
#include "accessmanager.h"
|
||||
#include "capabilities.h"
|
||||
#include "common/asserts.h"
|
||||
#include "cookiejar.h"
|
||||
#include "creds/abstractcredentials.h"
|
||||
#include "creds/credentialmanager.h"
|
||||
#include "graphapi/spacesmanager.h"
|
||||
#include "networkjobs.h"
|
||||
#include "networkjobs/resources.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include <QAuthenticator>
|
||||
#include <QDir>
|
||||
#include <QLoggingCategory>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkCookieJar>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QSslKey>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcAccount, "sync.account", QtInfoMsg)
|
||||
|
||||
QString Account::_customCommonCacheDirectory = {};
|
||||
|
||||
void Account::setCommonCacheDirectory(const QString &directory)
|
||||
{
|
||||
_customCommonCacheDirectory = directory;
|
||||
}
|
||||
|
||||
QString Account::commonCacheDirectory()
|
||||
{
|
||||
if (_customCommonCacheDirectory.isEmpty()) {
|
||||
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
}
|
||||
|
||||
return _customCommonCacheDirectory;
|
||||
}
|
||||
|
||||
Account::Account(const QUuid &uuid, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _uuid(uuid)
|
||||
, _capabilities({}, {})
|
||||
, _jobQueue(this)
|
||||
, _queueGuard(&_jobQueue)
|
||||
, _credentialManager(new CredentialManager(this))
|
||||
, _spacesManager(new GraphApi::SpacesManager(this))
|
||||
{
|
||||
qRegisterMetaType<AccountPtr>("AccountPtr");
|
||||
|
||||
_cacheDirectory = QStringLiteral("%1/accounts/%2").arg(commonCacheDirectory(), _uuid.toString(QUuid::WithoutBraces));
|
||||
QDir().mkpath(_cacheDirectory);
|
||||
|
||||
// we need to make sure the directory we pass to the resources cache exists
|
||||
const QString resourcesCacheDir = QStringLiteral("%1/resources/").arg(_cacheDirectory);
|
||||
QDir().mkpath(resourcesCacheDir);
|
||||
_resourcesCache = new ResourcesCache(resourcesCacheDir, this);
|
||||
}
|
||||
|
||||
AccountPtr Account::create(const QUuid &uuid)
|
||||
{
|
||||
AccountPtr acc = AccountPtr(new Account(uuid));
|
||||
acc->setSharedThis(acc);
|
||||
return acc;
|
||||
}
|
||||
|
||||
Account::~Account()
|
||||
{
|
||||
}
|
||||
|
||||
void Account::setSharedThis(AccountPtr sharedThis)
|
||||
{
|
||||
_sharedThis = sharedThis.toWeakRef();
|
||||
}
|
||||
|
||||
CredentialManager *Account::credentialManager() const
|
||||
{
|
||||
return _credentialManager;
|
||||
}
|
||||
|
||||
GraphApi::SpacesManager *Account::spacesManager() const
|
||||
{
|
||||
return _spacesManager;
|
||||
}
|
||||
|
||||
QUuid Account::uuid() const
|
||||
{
|
||||
return _uuid;
|
||||
}
|
||||
|
||||
AccountPtr Account::sharedFromThis()
|
||||
{
|
||||
return _sharedThis.toStrongRef();
|
||||
}
|
||||
|
||||
QIcon Account::avatar() const
|
||||
{
|
||||
return _avatarImg;
|
||||
}
|
||||
|
||||
void Account::setAvatar(const QIcon &img)
|
||||
{
|
||||
_avatarImg = img;
|
||||
Q_EMIT avatarChanged();
|
||||
}
|
||||
|
||||
bool Account::hasAvatar() const
|
||||
{
|
||||
return !_avatarImg.isNull();
|
||||
}
|
||||
|
||||
QString Account::displayNameWithHost() const
|
||||
{
|
||||
QString user = davDisplayName();
|
||||
QString host = _url.host();
|
||||
const int port = url().port();
|
||||
if (port > 0 && port != 80 && port != 443) {
|
||||
host += QStringLiteral(":%1").arg(QString::number(port));
|
||||
}
|
||||
return tr("%1@%2").arg(user, host);
|
||||
}
|
||||
|
||||
QString Account::initials() const
|
||||
{
|
||||
QString out;
|
||||
for (const auto &p : davDisplayName().split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
|
||||
out.append(p.first(1));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QGradient::Preset Account::avatarGradient() const
|
||||
{
|
||||
return static_cast<QGradient::Preset>(qHash(displayNameWithHost()) % QGradient::NumPresets + 1);
|
||||
}
|
||||
|
||||
QString Account::davDisplayName() const
|
||||
{
|
||||
return _displayName;
|
||||
}
|
||||
|
||||
void Account::setDavDisplayName(const QString &newDisplayName)
|
||||
{
|
||||
if (_displayName != newDisplayName) {
|
||||
_displayName = newDisplayName;
|
||||
Q_EMIT displayNameChanged();
|
||||
}
|
||||
}
|
||||
|
||||
AbstractCredentials *Account::credentials() const
|
||||
{
|
||||
return _credentials.data();
|
||||
}
|
||||
|
||||
void Account::setCredentials(AbstractCredentials *cred)
|
||||
{
|
||||
// set active credential manager
|
||||
QNetworkCookieJar *jar = nullptr;
|
||||
if (_am) {
|
||||
jar = _am->cookieJar();
|
||||
jar->setParent(nullptr);
|
||||
_am->deleteLater();
|
||||
}
|
||||
|
||||
// The order for these two is important! Reading the credential's
|
||||
// settings accesses the account as well as account->_credentials,
|
||||
_credentials.reset(cred);
|
||||
cred->setAccount(this);
|
||||
|
||||
_am = _credentials->createAM();
|
||||
|
||||
// the network access manager takes ownership when setCache is called, so we have to reinitialize it every time we reset the manager
|
||||
_networkCache = new QNetworkDiskCache(this);
|
||||
const QString networkCacheLocation = (QStringLiteral("%1/network/").arg(_cacheDirectory));
|
||||
qCDebug(lcAccount) << u"Cache location for account" << this << u"set to" << networkCacheLocation;
|
||||
_networkCache->setCacheDirectory(networkCacheLocation);
|
||||
_am->setCache(_networkCache);
|
||||
|
||||
if (jar) {
|
||||
_am->setCookieJar(jar);
|
||||
}
|
||||
connect(_credentials.data(), &AbstractCredentials::fetched, this, [this] {
|
||||
Q_EMIT credentialsFetched();
|
||||
_queueGuard.unblock();
|
||||
});
|
||||
connect(_credentials.data(), &AbstractCredentials::authenticationStarted, this, [this] {
|
||||
_queueGuard.block();
|
||||
});
|
||||
connect(_credentials.data(), &AbstractCredentials::authenticationFailed, this, [this] { _queueGuard.clear(); });
|
||||
}
|
||||
|
||||
/**
|
||||
* clear all cookies. (Session cookies or not)
|
||||
*/
|
||||
void Account::clearCookieJar()
|
||||
{
|
||||
qCInfo(lcAccount) << u"Clearing cookies";
|
||||
_am->cookieJar()->deleteLater();
|
||||
_am->setCookieJar(new CookieJar);
|
||||
}
|
||||
|
||||
AccessManager *Account::accessManager()
|
||||
{
|
||||
return _am.data();
|
||||
}
|
||||
|
||||
QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
|
||||
{
|
||||
Q_ASSERT(verb.isUpper());
|
||||
req.setUrl(url);
|
||||
if (verb == "HEAD" && !data) {
|
||||
return _am->head(req);
|
||||
} else if (verb == "GET" && !data) {
|
||||
return _am->get(req);
|
||||
} else if (verb == "POST") {
|
||||
return _am->post(req, data);
|
||||
} else if (verb == "PUT") {
|
||||
return _am->put(req, data);
|
||||
} else if (verb == "DELETE" && !data) {
|
||||
return _am->deleteResource(req);
|
||||
}
|
||||
return _am->sendCustomRequest(req, verb, data);
|
||||
}
|
||||
|
||||
void Account::setApprovedCerts(const QList<QSslCertificate> &certs)
|
||||
{
|
||||
_approvedCerts = { certs.begin(), certs.end() };
|
||||
_am->setCustomTrustedCaCertificates(_approvedCerts);
|
||||
}
|
||||
|
||||
void Account::addApprovedCerts(const QSet<QSslCertificate> &certs)
|
||||
{
|
||||
_approvedCerts.unite(certs);
|
||||
_am->setCustomTrustedCaCertificates(_approvedCerts);
|
||||
Q_EMIT wantsAccountSaved(this);
|
||||
}
|
||||
|
||||
void Account::setUrl(const QUrl &url)
|
||||
{
|
||||
if (_url != url) {
|
||||
_url = url;
|
||||
Q_EMIT urlChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QUrl Account::url() const
|
||||
{
|
||||
return _url;
|
||||
}
|
||||
|
||||
QString Account::hostName() const
|
||||
{
|
||||
return _url.host();
|
||||
}
|
||||
|
||||
JobQueue *Account::jobQueue()
|
||||
{
|
||||
return &_jobQueue;
|
||||
}
|
||||
|
||||
void Account::clearAMCache()
|
||||
{
|
||||
_am->clearAccessCache();
|
||||
}
|
||||
|
||||
const Capabilities &Account::capabilities() const
|
||||
{
|
||||
Q_ASSERT(hasCapabilities());
|
||||
return _capabilities;
|
||||
}
|
||||
|
||||
bool Account::hasCapabilities() const
|
||||
{
|
||||
return _capabilities.isValid();
|
||||
}
|
||||
|
||||
void Account::setCapabilities(const Capabilities &caps)
|
||||
{
|
||||
if (_capabilities != caps) {
|
||||
const bool versionChanged =
|
||||
caps.status().legacyVersion != _capabilities.status().legacyVersion || caps.status().productversion != _capabilities.status().productversion;
|
||||
_capabilities = caps;
|
||||
Q_EMIT capabilitiesChanged();
|
||||
if (versionChanged) {
|
||||
Q_EMIT serverVersionChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Account::ServerSupportLevel Account::serverSupportLevel() const
|
||||
{
|
||||
if (!hasCapabilities()) {
|
||||
// not detected yet, assume it is fine.
|
||||
return ServerSupportLevel::Supported;
|
||||
}
|
||||
|
||||
// A compatible server exposes a product version in its status capabilities.
|
||||
if (!capabilities().status().productversion.isEmpty()) {
|
||||
return ServerSupportLevel::Supported;
|
||||
}
|
||||
return ServerSupportLevel::Unsupported;
|
||||
}
|
||||
|
||||
bool Account::isHttp2Supported() const
|
||||
{
|
||||
return _http2Supported;
|
||||
}
|
||||
|
||||
void Account::setHttp2Supported(bool value)
|
||||
{
|
||||
_http2Supported = value;
|
||||
}
|
||||
|
||||
QString Account::defaultSyncRoot() const
|
||||
{
|
||||
Q_ASSERT(!_defaultSyncRoot.isEmpty());
|
||||
return _defaultSyncRoot;
|
||||
}
|
||||
bool Account::hasDefaultSyncRoot() const
|
||||
{
|
||||
return !_defaultSyncRoot.isEmpty();
|
||||
}
|
||||
|
||||
void Account::setDefaultSyncRoot(const QString &syncRoot)
|
||||
{
|
||||
Q_ASSERT(_defaultSyncRoot.isEmpty());
|
||||
if (!syncRoot.isEmpty()) {
|
||||
_defaultSyncRoot = syncRoot;
|
||||
}
|
||||
}
|
||||
|
||||
void Account::setAppProvider(AppProvider &&p)
|
||||
{
|
||||
_appProvider = std::move(p);
|
||||
}
|
||||
|
||||
const AppProvider &Account::appProvider() const
|
||||
{
|
||||
return _appProvider;
|
||||
}
|
||||
|
||||
void Account::invalidCredentialsEncountered()
|
||||
{
|
||||
Q_EMIT invalidCredentials(Account::QPrivateSignal());
|
||||
}
|
||||
|
||||
ResourcesCache *Account::resourcesCache() const
|
||||
{
|
||||
return _resourcesCache;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
|
||||
QDebug operator<<(QDebug debug, const OCC::Account *acc)
|
||||
{
|
||||
QDebugStateSaver saver(debug);
|
||||
debug.setAutoInsertSpaces(false);
|
||||
debug << u"OCC::Account(" << acc->displayNameWithHost() << u")";
|
||||
return debug.maybeSpace();
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
|
||||
#ifndef SERVERCONNECTION_H
|
||||
#define SERVERCONNECTION_H
|
||||
|
||||
#include "common/utility.h"
|
||||
|
||||
#include "appprovider.h"
|
||||
#include "capabilities.h"
|
||||
#include "jobqueue.h"
|
||||
#include "resources/resources.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QGradient>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkCookie>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkRequest>
|
||||
#include <QPixmap>
|
||||
#include <QSharedPointer>
|
||||
#include <QSslCertificate>
|
||||
#include <QSslCipher>
|
||||
#include <QSslConfiguration>
|
||||
#include <QSslError>
|
||||
#include <QSslSocket>
|
||||
#include <QUrl>
|
||||
#include <QUuid>
|
||||
#include <QtQmlIntegration/QtQmlIntegration>
|
||||
|
||||
class QSettings;
|
||||
class QNetworkReply;
|
||||
class QUrl;
|
||||
class AccessManager;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class CredentialManager;
|
||||
class AbstractCredentials;
|
||||
class Account;
|
||||
typedef QSharedPointer<Account> AccountPtr;
|
||||
class AccessManager;
|
||||
class SimpleNetworkJob;
|
||||
|
||||
namespace GraphApi {
|
||||
class SpacesManager;
|
||||
}
|
||||
|
||||
class ResourcesCache;
|
||||
|
||||
class QSFERA_SYNC_EXPORT Account : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QUuid uid READ uuid CONSTANT)
|
||||
Q_PROPERTY(QString davDisplayName READ davDisplayName NOTIFY displayNameChanged)
|
||||
Q_PROPERTY(QString displayNameWithHost READ displayNameWithHost NOTIFY displayNameChanged)
|
||||
Q_PROPERTY(QString initials READ initials NOTIFY displayNameChanged)
|
||||
Q_PROPERTY(QString hostName READ hostName NOTIFY urlChanged)
|
||||
Q_PROPERTY(bool hasAvatar READ hasAvatar NOTIFY avatarChanged)
|
||||
Q_PROPERTY(QGradient::Preset avatarGradient READ avatarGradient NOTIFY displayNameChanged)
|
||||
Q_PROPERTY(QUrl url READ url NOTIFY urlChanged)
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("Only created in the C++ code")
|
||||
|
||||
public:
|
||||
/**
|
||||
* Set a custom directory which all accounts created after this call will share to store their cached files in.
|
||||
*/
|
||||
static void setCommonCacheDirectory(const QString &directory);
|
||||
static QString commonCacheDirectory();
|
||||
|
||||
static AccountPtr create(const QUuid &uuid);
|
||||
~Account() override;
|
||||
|
||||
AccountPtr sharedFromThis();
|
||||
|
||||
/***
|
||||
* The default folder containing all spaces.
|
||||
* This function will assert if the sync root is empty.
|
||||
*/
|
||||
QString defaultSyncRoot() const;
|
||||
|
||||
/***
|
||||
* Whether we have defaultSyncRoot defined.
|
||||
*/
|
||||
bool hasDefaultSyncRoot() const;
|
||||
|
||||
/***
|
||||
* Set defaultSyncRoot and creates the path on the filesystem.
|
||||
* Setting an empty string will have no effect.
|
||||
*/
|
||||
void setDefaultSyncRoot(const QString &syncRoot);
|
||||
|
||||
QString davDisplayName() const;
|
||||
void setDavDisplayName(const QString &newDisplayName);
|
||||
|
||||
QIcon avatar() const;
|
||||
void setAvatar(const QIcon &img);
|
||||
bool hasAvatar() const;
|
||||
|
||||
/// The name of the account as shown in the toolbar
|
||||
QString displayNameWithHost() const;
|
||||
QString initials() const;
|
||||
QGradient::Preset avatarGradient() const;
|
||||
|
||||
/** Server url of the account */
|
||||
void setUrl(const QUrl &url);
|
||||
QUrl url() const;
|
||||
QString hostName() const;
|
||||
|
||||
/** Holds the accounts credentials */
|
||||
AbstractCredentials *credentials() const;
|
||||
void setCredentials(AbstractCredentials *cred);
|
||||
|
||||
/** Create a network request on the account's QNAM.
|
||||
*
|
||||
* Network requests in AbstractNetworkJobs are created through
|
||||
* this function. Other places should prefer to use jobs or
|
||||
* sendRequest().
|
||||
*/
|
||||
QNetworkReply *sendRawRequest(const QByteArray &verb,
|
||||
const QUrl &url,
|
||||
QNetworkRequest req = QNetworkRequest(),
|
||||
QIODevice *data = nullptr);
|
||||
|
||||
/** The certificates of the account */
|
||||
QSet<QSslCertificate> approvedCerts() const { return _approvedCerts; }
|
||||
|
||||
/***
|
||||
* Warning calling those will break running network jobs on the current access manager
|
||||
*/
|
||||
void setApprovedCerts(const QList<QSslCertificate> &certs);
|
||||
|
||||
/***
|
||||
* Warning calling those will break running network jobs on the current access manager
|
||||
*/
|
||||
void addApprovedCerts(const QSet<QSslCertificate> &certs);
|
||||
|
||||
/** Access the server capabilities */
|
||||
const Capabilities &capabilities() const;
|
||||
void setCapabilities(const Capabilities &caps);
|
||||
|
||||
bool hasCapabilities() const;
|
||||
|
||||
void setAppProvider(AppProvider &&p);
|
||||
const AppProvider &appProvider() const;
|
||||
|
||||
enum class ServerSupportLevel {
|
||||
Supported,
|
||||
Unknown,
|
||||
Unsupported
|
||||
};
|
||||
Q_ENUMS(ServerSupportLevel)
|
||||
ServerSupportLevel serverSupportLevel() const;
|
||||
|
||||
/** True when the server connection is using HTTP2 */
|
||||
bool isHttp2Supported() const;
|
||||
void setHttp2Supported(bool value);
|
||||
|
||||
void clearCookieJar();
|
||||
|
||||
AccessManager *accessManager();
|
||||
|
||||
JobQueue *jobQueue();
|
||||
|
||||
QUuid uuid() const;
|
||||
|
||||
CredentialManager *credentialManager() const;
|
||||
|
||||
GraphApi::SpacesManager *spacesManager() const;
|
||||
|
||||
/**
|
||||
* We encountered an authentication error.
|
||||
*/
|
||||
void invalidCredentialsEncountered();
|
||||
|
||||
ResourcesCache *resourcesCache() const;
|
||||
|
||||
public Q_SLOTS:
|
||||
/// Used when forgetting credentials
|
||||
void clearAMCache();
|
||||
|
||||
Q_SIGNALS:
|
||||
/// Triggered by invalidCredentialsEncountered()
|
||||
// this signal is emited when a network job failed due to invalid credentials
|
||||
void invalidCredentials(QPrivateSignal);
|
||||
|
||||
void credentialsFetched();
|
||||
void credentialsAsked();
|
||||
|
||||
// e.g. when the approved SSL certificates changed
|
||||
void wantsAccountSaved(Account *acc);
|
||||
|
||||
void serverVersionChanged();
|
||||
void capabilitiesChanged();
|
||||
|
||||
void avatarChanged();
|
||||
void displayNameChanged();
|
||||
|
||||
void unknownConnectionState();
|
||||
|
||||
void requestUrlUpdate(const QUrl &newUrl);
|
||||
|
||||
// the signal exists on the Account object as the Approvider itself can change during runtime
|
||||
void appProviderErrorOccured(const QString &error);
|
||||
|
||||
void urlChanged();
|
||||
|
||||
private:
|
||||
// directory all newly created accounts store their various caches in
|
||||
static QString _customCommonCacheDirectory;
|
||||
|
||||
Account(const QUuid &uuid, QObject *parent = nullptr);
|
||||
void setSharedThis(AccountPtr sharedThis);
|
||||
|
||||
QWeakPointer<Account> _sharedThis;
|
||||
QUuid _uuid;
|
||||
QString _displayName;
|
||||
QString _defaultSyncRoot;
|
||||
QIcon _avatarImg;
|
||||
QUrl _url;
|
||||
QString _cacheDirectory;
|
||||
|
||||
QSet<QSslCertificate> _approvedCerts;
|
||||
Capabilities _capabilities;
|
||||
QPointer<AccessManager> _am;
|
||||
QPointer<QNetworkDiskCache> _networkCache = nullptr;
|
||||
QPointer<ResourcesCache> _resourcesCache;
|
||||
QScopedPointer<AbstractCredentials> _credentials;
|
||||
bool _http2Supported = true;
|
||||
|
||||
JobQueue _jobQueue;
|
||||
JobQueueGuard _queueGuard;
|
||||
CredentialManager *_credentialManager;
|
||||
AppProvider _appProvider;
|
||||
|
||||
GraphApi::SpacesManager *_spacesManager = nullptr;
|
||||
friend class AccountManager;
|
||||
};
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(OCC::AccountPtr)
|
||||
|
||||
|
||||
QDebug QSFERA_SYNC_EXPORT operator<<(QDebug debug, const OCC::Account *job);
|
||||
|
||||
#endif //SERVERCONNECTION_H
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#ifndef SERVERFWD_H
|
||||
#define SERVERFWD_H
|
||||
|
||||
#include <QPointer>
|
||||
#include <QSharedPointer>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class Account;
|
||||
class AccountState;
|
||||
|
||||
using AccountPtr = QSharedPointer<Account>;
|
||||
using AccountStatePtr = QPointer<AccountState>;
|
||||
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#endif //SERVERFWD
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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 "appprovider.h"
|
||||
|
||||
#include "common/utility.h"
|
||||
#include "libsync/account.h"
|
||||
#include "libsync/networkjobs/jsonjob.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QJsonArray>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcAppProvider, "sync.appprovider", QtInfoMsg)
|
||||
|
||||
AppProvider::Provider::Provider(const QJsonObject &provider)
|
||||
: mimeType(provider.value(QStringLiteral("mime_type")).toString())
|
||||
, extension(provider.value(QStringLiteral("extension")).toString())
|
||||
, name(provider.value(QStringLiteral("name")).toString())
|
||||
, description(provider.value(QStringLiteral("description")).toString())
|
||||
, icon(provider.value(QStringLiteral("icon")).toString())
|
||||
, defaultApplication(provider.value(QStringLiteral("default_application")).toString())
|
||||
, allowCreation(provider.value(QStringLiteral("allow_creation")).toBool())
|
||||
{
|
||||
}
|
||||
|
||||
bool AppProvider::Provider::isValid() const
|
||||
{
|
||||
return !mimeType.isEmpty();
|
||||
}
|
||||
|
||||
AppProvider::AppProvider(const QJsonObject &apps)
|
||||
{
|
||||
const auto mimTypes = apps.value(QStringLiteral("mime-types")).toArray();
|
||||
_providers.reserve(apps.size());
|
||||
for (const auto &type : mimTypes) {
|
||||
Provider p(type.toObject());
|
||||
_providers.insert(p.mimeType, std::move(p));
|
||||
}
|
||||
}
|
||||
|
||||
const AppProvider::Provider &AppProvider::app(const QMimeType &mimeType) const
|
||||
{
|
||||
if (auto it = Utility::optionalFind(_providers, mimeType.name())) {
|
||||
return it->value();
|
||||
}
|
||||
static const AppProvider::Provider nullProvider { {} };
|
||||
return nullProvider;
|
||||
}
|
||||
|
||||
const AppProvider::Provider &AppProvider::app(const QString &localPath) const
|
||||
{
|
||||
QMimeDatabase db;
|
||||
auto mimeType = db.mimeTypeForFile(localPath);
|
||||
return app(mimeType);
|
||||
}
|
||||
|
||||
bool AppProvider::open(const AccountPtr &account, const QString &localPath, const QByteArray &fileId) const
|
||||
{
|
||||
const auto &a = app(localPath);
|
||||
if (a.isValid()) {
|
||||
SimpleNetworkJob::UrlQuery query { { QStringLiteral("file_id"), QString::fromUtf8(fileId) } };
|
||||
auto *job = new JsonJob(account, account->capabilities().appProviders().openWebUrl, {}, "POST", query);
|
||||
QObject::connect(job, &JsonJob::finishedSignal, account.data(), [account, job, localPath] {
|
||||
if (job->httpStatusCode() == 200) {
|
||||
const auto url = QUrl(job->data().value(QStringLiteral("uri")).toString());
|
||||
const auto result = QDesktopServices::openUrl(url);
|
||||
qCDebug(lcAppProvider) << u"start browser" << url << result;
|
||||
} else {
|
||||
Q_EMIT account->appProviderErrorOccured(
|
||||
QCoreApplication::translate("AppProvider", "Failed to open »%1« in web. Error: %2.").arg(localPath, job->reply()->errorString()));
|
||||
}
|
||||
});
|
||||
job->start();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "accountfwd.h"
|
||||
#include "capabilities.h"
|
||||
#include "qsferasynclib.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMimeType>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
#include <QVariantMap>
|
||||
|
||||
namespace OCC {
|
||||
class QSFERA_SYNC_EXPORT AppProvider
|
||||
{
|
||||
public:
|
||||
struct QSFERA_SYNC_EXPORT Provider
|
||||
{
|
||||
// the server might provide multiple apps but no default
|
||||
// for now we only support default apps
|
||||
Provider() = default;
|
||||
explicit Provider(const QJsonObject &provider);
|
||||
QString mimeType;
|
||||
QString extension;
|
||||
QString name;
|
||||
QString description;
|
||||
QUrl icon;
|
||||
QString defaultApplication;
|
||||
bool allowCreation = false;
|
||||
|
||||
bool isValid() const;
|
||||
};
|
||||
|
||||
explicit AppProvider(const QJsonObject &apps = {});
|
||||
|
||||
const Provider &app(const QMimeType &mimeType) const;
|
||||
const Provider &app(const QString &localPath) const;
|
||||
|
||||
bool open(const AccountPtr &account, const QString &localPath, const QByteArray &fileId) const;
|
||||
|
||||
|
||||
private:
|
||||
QHash<QString, Provider> _providers;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
* Copyright (C) by Markus Goetz <markus@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "libsync/networkjobs/getfilejob.h"
|
||||
#include "owncloudpropagator.h"
|
||||
#include "propagateupload.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QTimer>
|
||||
#include <QObject>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcBandwidthManager, "sync.bandwidthmanager", QtInfoMsg)
|
||||
|
||||
// Because of the many layers of buffering inside Qt (and probably the OS and the network)
|
||||
// we cannot lower this value much more. If we do, the estimated bw will be very high
|
||||
// because the buffers fill fast while the actual network algorithms are not relevant yet.
|
||||
static qint64 relativeLimitMeasuringTimerIntervalMsec = 1000 * 2;
|
||||
// See also WritingState in http://code.woboq.org/qt5/qtbase/src/network/access/qhttpprotocolhandler.cpp.html#_ZN20QHttpProtocolHandler11sendRequestEv
|
||||
|
||||
// FIXME At some point:
|
||||
// * Register device only after the QNR received its metaDataChanged() signal
|
||||
// * Incorporate Qt buffer fill state (it's a negative absolute delta).
|
||||
// * Incorporate SSL overhead (percentage)
|
||||
// * For relative limiting, do less measuring and more delaying+giving quota
|
||||
// * For relative limiting, smoothen measurements
|
||||
|
||||
BandwidthManager::BandwidthManager(OwncloudPropagator *p)
|
||||
: QObject(p)
|
||||
, _propagator(p)
|
||||
, _relativeLimitCurrentMeasuredDevice(nullptr)
|
||||
, _relativeUploadLimitProgressAtMeasuringRestart(0)
|
||||
, _currentUploadLimit(0)
|
||||
, _relativeLimitCurrentMeasuredJob(nullptr)
|
||||
, _currentDownloadLimit(0)
|
||||
{
|
||||
// absolute uploads/downloads
|
||||
QObject::connect(&_absoluteLimitTimer, &QTimer::timeout, this, &BandwidthManager::absoluteLimitTimerExpired);
|
||||
_absoluteLimitTimer.setInterval(1000);
|
||||
_absoluteLimitTimer.start();
|
||||
|
||||
// Relative uploads
|
||||
QObject::connect(&_relativeUploadMeasuringTimer, &QTimer::timeout,
|
||||
this, &BandwidthManager::relativeUploadMeasuringTimerExpired);
|
||||
_relativeUploadMeasuringTimer.setInterval(relativeLimitMeasuringTimerIntervalMsec);
|
||||
_relativeUploadMeasuringTimer.start();
|
||||
_relativeUploadMeasuringTimer.setSingleShot(true); // will be restarted from the delay timer
|
||||
QObject::connect(&_relativeUploadDelayTimer, &QTimer::timeout,
|
||||
this, &BandwidthManager::relativeUploadDelayTimerExpired);
|
||||
_relativeUploadDelayTimer.setSingleShot(true); // will be restarted from the measuring timer
|
||||
|
||||
// Relative downloads
|
||||
QObject::connect(&_relativeDownloadMeasuringTimer, &QTimer::timeout,
|
||||
this, &BandwidthManager::relativeDownloadMeasuringTimerExpired);
|
||||
_relativeDownloadMeasuringTimer.setInterval(relativeLimitMeasuringTimerIntervalMsec);
|
||||
_relativeDownloadMeasuringTimer.start();
|
||||
_relativeDownloadMeasuringTimer.setSingleShot(true); // will be restarted from the delay timer
|
||||
QObject::connect(&_relativeDownloadDelayTimer, &QTimer::timeout,
|
||||
this, &BandwidthManager::relativeDownloadDelayTimerExpired);
|
||||
_relativeDownloadDelayTimer.setSingleShot(true); // will be restarted from the measuring timer
|
||||
}
|
||||
|
||||
BandwidthManager::~BandwidthManager()
|
||||
{
|
||||
}
|
||||
|
||||
void BandwidthManager::registerUploadDevice(UploadDevice *p)
|
||||
{
|
||||
_absoluteUploadDeviceList.push_back(p);
|
||||
_relativeUploadDeviceList.push_back(p);
|
||||
QObject::connect(p, &QObject::destroyed, this, &BandwidthManager::unregisterUploadDevice);
|
||||
|
||||
if (usingAbsoluteUploadLimit()) {
|
||||
p->setBandwidthLimited(true);
|
||||
p->setChoked(false);
|
||||
} else if (usingRelativeUploadLimit()) {
|
||||
p->setBandwidthLimited(true);
|
||||
p->setChoked(true);
|
||||
} else {
|
||||
p->setBandwidthLimited(false);
|
||||
p->setChoked(false);
|
||||
}
|
||||
}
|
||||
|
||||
void BandwidthManager::unregisterUploadDevice(QObject *o)
|
||||
{
|
||||
auto p = reinterpret_cast<UploadDevice *>(o); // note, we might already be in the ~QObject
|
||||
_absoluteUploadDeviceList.remove(p);
|
||||
_relativeUploadDeviceList.remove(p);
|
||||
if (p == _relativeLimitCurrentMeasuredDevice) {
|
||||
_relativeLimitCurrentMeasuredDevice = nullptr;
|
||||
_relativeUploadLimitProgressAtMeasuringRestart = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void BandwidthManager::registerDownloadJob(GETFileJob *j)
|
||||
{
|
||||
_downloadJobList.push_back(j);
|
||||
connect(j, &GETFileJob::aboutToFinishSignal, this, [j, this] {
|
||||
unregisterDownloadJob(j);
|
||||
});
|
||||
|
||||
if (usingAbsoluteDownloadLimit()) {
|
||||
j->setBandwidthLimited(true);
|
||||
j->setChoked(false);
|
||||
} else if (usingRelativeDownloadLimit()) {
|
||||
j->setBandwidthLimited(true);
|
||||
j->setChoked(true);
|
||||
} else {
|
||||
j->setBandwidthLimited(false);
|
||||
j->setChoked(false);
|
||||
}
|
||||
}
|
||||
|
||||
void BandwidthManager::unregisterDownloadJob(GETFileJob *j)
|
||||
{
|
||||
j->setChoked(false);
|
||||
j->setBandwidthLimited(false);
|
||||
_downloadJobList.remove(j);
|
||||
if (_relativeLimitCurrentMeasuredJob == j) {
|
||||
_relativeLimitCurrentMeasuredJob = nullptr;
|
||||
_relativeDownloadLimitProgressAtMeasuringRestart = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void BandwidthManager::relativeUploadMeasuringTimerExpired()
|
||||
{
|
||||
if (!usingRelativeUploadLimit() || _relativeUploadDeviceList.empty()) {
|
||||
// Not in this limiting mode, just wait 1 sec to continue the cycle
|
||||
_relativeUploadDelayTimer.setInterval(1000);
|
||||
_relativeUploadDelayTimer.start();
|
||||
return;
|
||||
}
|
||||
if (_relativeLimitCurrentMeasuredDevice == nullptr) {
|
||||
qCDebug(lcBandwidthManager) << u"No device set, just waiting 1 sec";
|
||||
_relativeUploadDelayTimer.setInterval(1000);
|
||||
_relativeUploadDelayTimer.start();
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(lcBandwidthManager) << _relativeUploadDeviceList.size() << u"Starting Delay";
|
||||
|
||||
qint64 relativeLimitProgressMeasured = (_relativeLimitCurrentMeasuredDevice->_readWithProgress
|
||||
+ _relativeLimitCurrentMeasuredDevice->_read)
|
||||
/ 2;
|
||||
qint64 relativeLimitProgressDifference = relativeLimitProgressMeasured - _relativeUploadLimitProgressAtMeasuringRestart;
|
||||
qCDebug(lcBandwidthManager) << _relativeUploadLimitProgressAtMeasuringRestart
|
||||
<< relativeLimitProgressMeasured << relativeLimitProgressDifference;
|
||||
|
||||
qint64 speedkBPerSec = (relativeLimitProgressDifference / relativeLimitMeasuringTimerIntervalMsec * 1000.0) / 1024.0;
|
||||
qCDebug(lcBandwidthManager) << relativeLimitProgressDifference / 1024 << u"kB =>" << speedkBPerSec << u"kB/sec on full speed ("
|
||||
<< _relativeLimitCurrentMeasuredDevice->_readWithProgress << _relativeLimitCurrentMeasuredDevice->_read
|
||||
<< qAbs(_relativeLimitCurrentMeasuredDevice->_readWithProgress - _relativeLimitCurrentMeasuredDevice->_read) << u")";
|
||||
|
||||
qint64 uploadLimitPercent = -_currentUploadLimit;
|
||||
// don't use too extreme values
|
||||
uploadLimitPercent = qMin(uploadLimitPercent, qint64(90));
|
||||
uploadLimitPercent = qMax(qint64(10), uploadLimitPercent);
|
||||
qint64 wholeTimeMsec = (100.0 / uploadLimitPercent) * relativeLimitMeasuringTimerIntervalMsec;
|
||||
qint64 waitTimeMsec = wholeTimeMsec - relativeLimitMeasuringTimerIntervalMsec;
|
||||
qint64 realWaitTimeMsec = waitTimeMsec + wholeTimeMsec;
|
||||
qCDebug(lcBandwidthManager) << waitTimeMsec << u" - " << realWaitTimeMsec << u" msec for " << uploadLimitPercent << u"%";
|
||||
|
||||
// We want to wait twice as long since we want to give all
|
||||
// devices the same quota we used now since we don't want
|
||||
// any upload to timeout
|
||||
_relativeUploadDelayTimer.setInterval(realWaitTimeMsec);
|
||||
_relativeUploadDelayTimer.start();
|
||||
|
||||
auto deviceCount = _relativeUploadDeviceList.size();
|
||||
qint64 quotaPerDevice = relativeLimitProgressDifference * (uploadLimitPercent / 100.0) / deviceCount + 1.0;
|
||||
for (auto *ud : _relativeUploadDeviceList) {
|
||||
ud->setBandwidthLimited(true);
|
||||
ud->setChoked(false);
|
||||
ud->giveBandwidthQuota(quotaPerDevice);
|
||||
qCDebug(lcBandwidthManager) << u"Gave" << quotaPerDevice / 1024.0 << u"kB to" << ud;
|
||||
}
|
||||
_relativeLimitCurrentMeasuredDevice = nullptr;
|
||||
}
|
||||
|
||||
void BandwidthManager::relativeUploadDelayTimerExpired()
|
||||
{
|
||||
// Switch to measuring state
|
||||
_relativeUploadMeasuringTimer.start(); // always start to continue the cycle
|
||||
|
||||
if (!usingRelativeUploadLimit()) {
|
||||
return; // oh, not actually needed
|
||||
}
|
||||
|
||||
if (_relativeUploadDeviceList.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(lcBandwidthManager) << _relativeUploadDeviceList.size() << u"Starting measuring";
|
||||
|
||||
// Take first device and then append it again (= we round robin all devices)
|
||||
_relativeLimitCurrentMeasuredDevice = _relativeUploadDeviceList.front();
|
||||
_relativeUploadDeviceList.pop_front();
|
||||
_relativeUploadDeviceList.push_back(_relativeLimitCurrentMeasuredDevice);
|
||||
|
||||
_relativeUploadLimitProgressAtMeasuringRestart = (_relativeLimitCurrentMeasuredDevice->_readWithProgress
|
||||
+ _relativeLimitCurrentMeasuredDevice->_read)
|
||||
/ 2;
|
||||
_relativeLimitCurrentMeasuredDevice->setBandwidthLimited(false);
|
||||
_relativeLimitCurrentMeasuredDevice->setChoked(false);
|
||||
|
||||
// choke all other UploadDevices
|
||||
for (auto *ud : std::as_const(_relativeUploadDeviceList)) {
|
||||
if (ud != _relativeLimitCurrentMeasuredDevice) {
|
||||
ud->setBandwidthLimited(true);
|
||||
ud->setChoked(true);
|
||||
}
|
||||
}
|
||||
|
||||
// now we're in measuring state
|
||||
}
|
||||
|
||||
qint64 BandwidthManager::currentUploadLimit() const
|
||||
{
|
||||
return _currentUploadLimit;
|
||||
}
|
||||
|
||||
void BandwidthManager::setCurrentUploadLimit(qint64 newUploadLimit)
|
||||
{
|
||||
if (newUploadLimit != _currentUploadLimit) {
|
||||
qCInfo(lcBandwidthManager) << u"Upload Bandwidth limit changed" << _currentUploadLimit << newUploadLimit;
|
||||
_currentUploadLimit = newUploadLimit;
|
||||
for (auto *ud : _relativeUploadDeviceList) {
|
||||
if (newUploadLimit == 0) {
|
||||
ud->setBandwidthLimited(false);
|
||||
ud->setChoked(false);
|
||||
} else if (newUploadLimit > 0) {
|
||||
ud->setBandwidthLimited(true);
|
||||
ud->setChoked(false);
|
||||
} else if (newUploadLimit < 0) {
|
||||
ud->setBandwidthLimited(true);
|
||||
ud->setChoked(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for downloads:
|
||||
void BandwidthManager::relativeDownloadMeasuringTimerExpired()
|
||||
{
|
||||
if (!usingRelativeDownloadLimit() || _downloadJobList.empty()) {
|
||||
// Not in this limiting mode, just wait 1 sec to continue the cycle
|
||||
_relativeDownloadDelayTimer.setInterval(1000);
|
||||
_relativeDownloadDelayTimer.start();
|
||||
return;
|
||||
}
|
||||
if (_relativeLimitCurrentMeasuredJob == nullptr) {
|
||||
qCDebug(lcBandwidthManager) << u"No job set, just waiting 1 sec";
|
||||
_relativeDownloadDelayTimer.setInterval(1000);
|
||||
_relativeDownloadDelayTimer.start();
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(lcBandwidthManager) << _downloadJobList.size() << u"Starting Delay";
|
||||
|
||||
qint64 relativeLimitProgressMeasured = _relativeLimitCurrentMeasuredJob->currentDownloadPosition();
|
||||
qint64 relativeLimitProgressDifference = relativeLimitProgressMeasured - _relativeDownloadLimitProgressAtMeasuringRestart;
|
||||
qCDebug(lcBandwidthManager) << _relativeDownloadLimitProgressAtMeasuringRestart
|
||||
<< relativeLimitProgressMeasured << relativeLimitProgressDifference;
|
||||
|
||||
qint64 speedkBPerSec = (relativeLimitProgressDifference / relativeLimitMeasuringTimerIntervalMsec * 1000.0) / 1024.0;
|
||||
qCDebug(lcBandwidthManager) << relativeLimitProgressDifference / 1024 << u"kB =>" << speedkBPerSec << u"kB/sec on full speed ("
|
||||
<< _relativeLimitCurrentMeasuredJob->currentDownloadPosition();
|
||||
|
||||
qint64 downloadLimitPercent = -_currentDownloadLimit;
|
||||
// don't use too extreme values
|
||||
downloadLimitPercent = qMin(downloadLimitPercent, qint64(90));
|
||||
downloadLimitPercent = qMax(qint64(10), downloadLimitPercent);
|
||||
qint64 wholeTimeMsec = (100.0 / downloadLimitPercent) * relativeLimitMeasuringTimerIntervalMsec;
|
||||
qint64 waitTimeMsec = wholeTimeMsec - relativeLimitMeasuringTimerIntervalMsec;
|
||||
qint64 realWaitTimeMsec = waitTimeMsec + wholeTimeMsec;
|
||||
qCDebug(lcBandwidthManager) << waitTimeMsec << u" - " << realWaitTimeMsec << u" msec for " << downloadLimitPercent << u"%";
|
||||
|
||||
// We want to wait twice as long since we want to give all
|
||||
// devices the same quota we used now since we don't want
|
||||
// any download to timeout
|
||||
_relativeDownloadDelayTimer.setInterval(realWaitTimeMsec);
|
||||
_relativeDownloadDelayTimer.start();
|
||||
|
||||
auto jobCount = _downloadJobList.size();
|
||||
qint64 quota = relativeLimitProgressDifference * (downloadLimitPercent / 100.0);
|
||||
if (quota > 20 * 1024) {
|
||||
qCInfo(lcBandwidthManager) << u"ADJUSTING QUOTA FROM " << quota << u" TO " << quota - 20 * 1024;
|
||||
quota -= 20 * 1024;
|
||||
}
|
||||
qint64 quotaPerJob = quota / jobCount + 1.0;
|
||||
for (auto *gfj : _downloadJobList) {
|
||||
gfj->setBandwidthLimited(true);
|
||||
gfj->setChoked(false);
|
||||
gfj->giveBandwidthQuota(quotaPerJob);
|
||||
qCDebug(lcBandwidthManager) << u"Gave" << quotaPerJob / 1024.0 << u"kB to" << gfj;
|
||||
}
|
||||
_relativeLimitCurrentMeasuredDevice = nullptr;
|
||||
}
|
||||
|
||||
void BandwidthManager::relativeDownloadDelayTimerExpired()
|
||||
{
|
||||
// Switch to measuring state
|
||||
_relativeDownloadMeasuringTimer.start(); // always start to continue the cycle
|
||||
|
||||
if (!usingRelativeDownloadLimit()) {
|
||||
return; // oh, not actually needed
|
||||
}
|
||||
|
||||
if (_downloadJobList.empty()) {
|
||||
qCDebug(lcBandwidthManager) << _downloadJobList.size() << u"No jobs?";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(lcBandwidthManager) << _downloadJobList.size() << u"Starting measuring";
|
||||
|
||||
// Take first device and then append it again (= we round robin all devices)
|
||||
_relativeLimitCurrentMeasuredJob = _downloadJobList.front();
|
||||
_downloadJobList.pop_front();
|
||||
_downloadJobList.push_back(_relativeLimitCurrentMeasuredJob);
|
||||
|
||||
_relativeDownloadLimitProgressAtMeasuringRestart = _relativeLimitCurrentMeasuredJob->currentDownloadPosition();
|
||||
_relativeLimitCurrentMeasuredJob->setBandwidthLimited(false);
|
||||
_relativeLimitCurrentMeasuredJob->setChoked(false);
|
||||
|
||||
// choke all other download jobs
|
||||
for (auto *gfj : _downloadJobList) {
|
||||
if (gfj != _relativeLimitCurrentMeasuredJob) {
|
||||
gfj->setBandwidthLimited(true);
|
||||
gfj->setChoked(true);
|
||||
}
|
||||
}
|
||||
|
||||
// now we're in measuring state
|
||||
}
|
||||
|
||||
qint64 BandwidthManager::currentDownloadLimit() const
|
||||
{
|
||||
return _currentDownloadLimit;
|
||||
}
|
||||
|
||||
void BandwidthManager::setCurrentDownloadLimit(qint64 newDownloadLimit)
|
||||
{
|
||||
if (newDownloadLimit != _currentDownloadLimit) {
|
||||
qCInfo(lcBandwidthManager) << u"Download Bandwidth limit changed" << _currentDownloadLimit << newDownloadLimit;
|
||||
_currentDownloadLimit = newDownloadLimit;
|
||||
for (auto *j : _downloadJobList) {
|
||||
if (usingAbsoluteDownloadLimit()) {
|
||||
j->setBandwidthLimited(true);
|
||||
j->setChoked(false);
|
||||
} else if (usingRelativeDownloadLimit()) {
|
||||
j->setBandwidthLimited(true);
|
||||
j->setChoked(true);
|
||||
} else {
|
||||
j->setBandwidthLimited(false);
|
||||
j->setChoked(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// end downloads
|
||||
|
||||
void BandwidthManager::absoluteLimitTimerExpired()
|
||||
{
|
||||
if (usingAbsoluteUploadLimit() && !_absoluteUploadDeviceList.empty()) {
|
||||
qint64 quotaPerDevice = _currentUploadLimit / _absoluteUploadDeviceList.size();
|
||||
qCDebug(lcBandwidthManager) << quotaPerDevice << _absoluteUploadDeviceList.size() << _currentUploadLimit;
|
||||
for (auto *device : std::as_const(_absoluteUploadDeviceList)) {
|
||||
device->giveBandwidthQuota(quotaPerDevice);
|
||||
qCDebug(lcBandwidthManager) << u"Gave " << quotaPerDevice / 1024.0 << u" kB to" << device;
|
||||
}
|
||||
}
|
||||
if (usingAbsoluteDownloadLimit() && !_downloadJobList.empty()) {
|
||||
qint64 quotaPerJob = _currentDownloadLimit / _downloadJobList.size();
|
||||
qCDebug(lcBandwidthManager) << quotaPerJob << _downloadJobList.size() << _currentDownloadLimit;
|
||||
for (auto *j : _downloadJobList) {
|
||||
j->giveBandwidthQuota(quotaPerJob);
|
||||
qCDebug(lcBandwidthManager) << u"Gave " << quotaPerJob / 1024.0 << u" kB to" << j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) by Markus Goetz <markus@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#ifndef BANDWIDTHMANAGER_H
|
||||
#define BANDWIDTHMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
#include <QIODevice>
|
||||
|
||||
#include <list>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class UploadDevice;
|
||||
class GETFileJob;
|
||||
class OwncloudPropagator;
|
||||
|
||||
/**
|
||||
* @brief The BandwidthManager class
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class BandwidthManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
BandwidthManager(OwncloudPropagator *p);
|
||||
~BandwidthManager() override;
|
||||
|
||||
bool usingAbsoluteUploadLimit() { return _currentUploadLimit > 0; }
|
||||
bool usingRelativeUploadLimit() { return _currentUploadLimit < 0; }
|
||||
bool usingAbsoluteDownloadLimit() { return _currentDownloadLimit > 0; }
|
||||
bool usingRelativeDownloadLimit() { return _currentDownloadLimit < 0; }
|
||||
|
||||
|
||||
qint64 currentDownloadLimit() const;
|
||||
void setCurrentDownloadLimit(qint64 newCurrentDownloadLimit);
|
||||
|
||||
qint64 currentUploadLimit() const;
|
||||
void setCurrentUploadLimit(qint64 newCurrentUploadLimit);
|
||||
|
||||
public Q_SLOTS:
|
||||
void registerUploadDevice(UploadDevice *);
|
||||
void unregisterUploadDevice(QObject *);
|
||||
|
||||
void registerDownloadJob(GETFileJob *);
|
||||
void unregisterDownloadJob(GETFileJob *);
|
||||
|
||||
void absoluteLimitTimerExpired();
|
||||
|
||||
void relativeUploadMeasuringTimerExpired();
|
||||
void relativeUploadDelayTimerExpired();
|
||||
|
||||
void relativeDownloadMeasuringTimerExpired();
|
||||
void relativeDownloadDelayTimerExpired();
|
||||
|
||||
private:
|
||||
// FIXME this timer and this variable should be replaced
|
||||
// by the propagator emitting the changed limit values to us as signal
|
||||
OwncloudPropagator *_propagator;
|
||||
|
||||
// for absolute up/down bw limiting
|
||||
QTimer _absoluteLimitTimer;
|
||||
|
||||
// FIXME merge these two lists
|
||||
std::list<UploadDevice *> _absoluteUploadDeviceList;
|
||||
std::list<UploadDevice *> _relativeUploadDeviceList;
|
||||
|
||||
QTimer _relativeUploadMeasuringTimer;
|
||||
|
||||
// for relative bw limiting, we need to wait this amount before measuring again
|
||||
QTimer _relativeUploadDelayTimer;
|
||||
|
||||
// the device measured
|
||||
UploadDevice *_relativeLimitCurrentMeasuredDevice;
|
||||
|
||||
// for measuring how much progress we made at start
|
||||
qint64 _relativeUploadLimitProgressAtMeasuringRestart;
|
||||
qint64 _currentUploadLimit;
|
||||
|
||||
std::list<GETFileJob *> _downloadJobList;
|
||||
QTimer _relativeDownloadMeasuringTimer;
|
||||
|
||||
// for relative bw limiting, we need to wait this amount before measuring again
|
||||
QTimer _relativeDownloadDelayTimer;
|
||||
|
||||
// the device measured
|
||||
GETFileJob *_relativeLimitCurrentMeasuredJob;
|
||||
|
||||
// for measuring how much progress we made at start
|
||||
qint64 _relativeDownloadLimitProgressAtMeasuringRestart;
|
||||
|
||||
qint64 _currentDownloadLimit;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "capabilities.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QVariantMap>
|
||||
|
||||
using namespace std::chrono;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
|
||||
Capabilities::Capabilities(const QUrl &url, const QVariantMap &capabilities)
|
||||
: _capabilities(capabilities)
|
||||
, _fileSharingCapabilities(_capabilities.value(QStringLiteral("files_sharing")).toMap())
|
||||
, _fileSharingPublicCapabilities(_fileSharingCapabilities.value(QStringLiteral("public"), {}).toMap())
|
||||
, _tusSupport(_capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("tus_support")).toMap())
|
||||
, _spaces(_capabilities.value(QStringLiteral("spaces")).toMap())
|
||||
, _status(_capabilities.value(QStringLiteral("core")).toMap().value(QStringLiteral("status")).toMap())
|
||||
, _checkForUpdates(_capabilities.value("core"_L1).toMap().value("check_for_updates"_L1, true).toBool())
|
||||
, _appProviders(AppProviders::findVersion(
|
||||
url, _capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("app_providers")).toList(), QVersionNumber({1, 1, 0})))
|
||||
, _filesSharing(_fileSharingCapabilities)
|
||||
, _migration(_capabilities.value(QStringLiteral("migration")).toMap())
|
||||
{
|
||||
}
|
||||
|
||||
QVariantMap Capabilities::raw() const
|
||||
{
|
||||
return _capabilities;
|
||||
}
|
||||
|
||||
bool Capabilities::operator==(const Capabilities &other) const
|
||||
{
|
||||
return raw() == other.raw();
|
||||
}
|
||||
|
||||
bool Capabilities::shareAPI() const
|
||||
{
|
||||
// This was later added so if it is not present just assume the API is enabled.
|
||||
return _fileSharingCapabilities.value(QStringLiteral("api_enabled"), true).toBool();
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLink() const
|
||||
{
|
||||
// This was later added so if it is not present just assume that link sharing is enabled.
|
||||
return shareAPI() && _fileSharingPublicCapabilities.value(QStringLiteral("enabled"), true).toBool();
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLinkAllowUpload() const
|
||||
{
|
||||
return _fileSharingPublicCapabilities.value(QStringLiteral("upload")).toBool();
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLinkSupportsUploadOnly() const
|
||||
{
|
||||
return _fileSharingPublicCapabilities.value(QStringLiteral("supports_upload_only")).toBool();
|
||||
}
|
||||
|
||||
static bool getEnforcePasswordCapability(const QVariantMap &fileSharingPublicCapabilities, const QString &name)
|
||||
{
|
||||
auto value = fileSharingPublicCapabilities[QStringLiteral("password")].toMap()[QStringLiteral("enforced_for")].toMap()[name];
|
||||
if (!value.isValid())
|
||||
return fileSharingPublicCapabilities[QStringLiteral("password")].toMap()[QStringLiteral("enforced")].toBool();
|
||||
return value.toBool();
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLinkEnforcePasswordForReadOnly() const
|
||||
{
|
||||
return getEnforcePasswordCapability(_fileSharingPublicCapabilities, QStringLiteral("read_only"));
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLinkEnforcePasswordForReadWrite() const
|
||||
{
|
||||
return getEnforcePasswordCapability(_fileSharingPublicCapabilities, QStringLiteral("read_write"));
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLinkEnforcePasswordForUploadOnly() const
|
||||
{
|
||||
return getEnforcePasswordCapability(_fileSharingPublicCapabilities, QStringLiteral("upload_only"));
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLinkDefaultExpire() const
|
||||
{
|
||||
return _fileSharingPublicCapabilities.value(QStringLiteral("expire_date")).toMap().value(QStringLiteral("enabled")).toBool();
|
||||
}
|
||||
|
||||
int Capabilities::sharePublicLinkDefaultExpireDateDays() const
|
||||
{
|
||||
return _fileSharingPublicCapabilities.value(QStringLiteral("expire_date")).toMap().value(QStringLiteral("days")).toInt();
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLinkEnforceExpireDate() const
|
||||
{
|
||||
return _fileSharingPublicCapabilities.value(QStringLiteral("expire_date")).toMap().value(QStringLiteral("enforced")).toBool();
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLinkMultiple() const
|
||||
{
|
||||
return _fileSharingPublicCapabilities.value(QStringLiteral("multiple")).toBool();
|
||||
}
|
||||
|
||||
bool Capabilities::shareResharing() const
|
||||
{
|
||||
return _fileSharingCapabilities.value(QStringLiteral("resharing")).toBool();
|
||||
}
|
||||
|
||||
int Capabilities::defaultPermissions() const
|
||||
{
|
||||
return _fileSharingCapabilities.value(QStringLiteral("default_permissions"), 1).toInt();
|
||||
}
|
||||
|
||||
std::chrono::seconds Capabilities::remotePollInterval() const
|
||||
{
|
||||
return duration_cast<seconds>(milliseconds(_capabilities.value(QStringLiteral("core")).toMap().value(QStringLiteral("pollinterval")).toInt()));
|
||||
}
|
||||
|
||||
bool Capabilities::notificationsAvailable() const
|
||||
{
|
||||
// We require the OCS style API in 9.x, can't deal with the REST one only found in 8.2
|
||||
return _capabilities.contains(QStringLiteral("notifications")) && _capabilities.value(QStringLiteral("notifications")).toMap().contains(QStringLiteral("ocs-endpoints"));
|
||||
}
|
||||
|
||||
bool Capabilities::isValid() const
|
||||
{
|
||||
return !_capabilities.isEmpty();
|
||||
}
|
||||
|
||||
QList<CheckSums::Algorithm> Capabilities::supportedChecksumTypes() const
|
||||
{
|
||||
// TODO: compute once
|
||||
QList<CheckSums::Algorithm> list;
|
||||
const auto &supportedTypes = _capabilities.value(QStringLiteral("checksums")).toMap().value(QStringLiteral("supportedTypes")).toList();
|
||||
for (const auto &t : supportedTypes) {
|
||||
list.push_back(CheckSums::fromByteArray(t.toByteArray()));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
CheckSums::Algorithm Capabilities::preferredUploadChecksumType() const
|
||||
{
|
||||
return CheckSums::fromByteArray(
|
||||
_capabilities.value(QStringLiteral("checksums")).toMap().value(QStringLiteral("preferredUploadType"), QStringLiteral("SHA1")).toByteArray());
|
||||
}
|
||||
|
||||
CheckSums::Algorithm Capabilities::uploadChecksumType() const
|
||||
{
|
||||
auto preferred = preferredUploadChecksumType();
|
||||
if (preferred != CheckSums::Algorithm::PARSE_ERROR)
|
||||
return preferred;
|
||||
QList<CheckSums::Algorithm> supported = supportedChecksumTypes();
|
||||
if (!supported.isEmpty())
|
||||
return supported.first();
|
||||
return CheckSums::Algorithm::PARSE_ERROR;
|
||||
}
|
||||
|
||||
const Status &Capabilities::status() const
|
||||
{
|
||||
return _status;
|
||||
}
|
||||
|
||||
bool Capabilities::checkForUpdates() const
|
||||
{
|
||||
return _checkForUpdates;
|
||||
}
|
||||
|
||||
const TusSupport &Capabilities::tusSupport() const
|
||||
{
|
||||
return _tusSupport;
|
||||
}
|
||||
|
||||
const SpaceSupport &Capabilities::spacesSupport() const
|
||||
{
|
||||
return _spaces;
|
||||
}
|
||||
|
||||
bool Capabilities::privateLinkPropertyAvailable() const
|
||||
{
|
||||
return _capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("privateLinks")).toBool();
|
||||
}
|
||||
|
||||
bool Capabilities::privateLinkDetailsParamAvailable() const
|
||||
{
|
||||
return _capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("privateLinksDetailsParam")).toBool();
|
||||
}
|
||||
|
||||
QList<int> Capabilities::httpErrorCodesThatResetFailingChunkedUploads() const
|
||||
{
|
||||
QList<int> list;
|
||||
const auto &errorCodes = _capabilities.value(QStringLiteral("dav")).toMap().value(QStringLiteral("httpErrorCodesThatResetFailingChunkedUploads")).toList();
|
||||
for (const auto &t : errorCodes) {
|
||||
list.push_back(t.toInt());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
QString Capabilities::invalidFilenameRegex() const
|
||||
{
|
||||
return _capabilities[QStringLiteral("dav")].toMap()[QStringLiteral("invalidFilenameRegex")].toString();
|
||||
}
|
||||
|
||||
bool Capabilities::uploadConflictFiles() const
|
||||
{
|
||||
return _capabilities[QStringLiteral("uploadConflictFiles")].toBool();
|
||||
}
|
||||
|
||||
bool Capabilities::versioningEnabled() const
|
||||
{
|
||||
return _capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("versioning")).toBool();
|
||||
}
|
||||
|
||||
QStringList Capabilities::blacklistedFiles() const
|
||||
{
|
||||
return _capabilities.value(QStringLiteral("files")).toMap().value(QStringLiteral("blacklisted_files")).toStringList();
|
||||
}
|
||||
|
||||
Status::Status(const QVariantMap &status)
|
||||
{
|
||||
legacyVersion = QVersionNumber::fromString(status.value(QStringLiteral("version")).toString());
|
||||
legacyVersionString = status.value(QStringLiteral("versionstring")).toString();
|
||||
edition = status.value(QStringLiteral("edition")).toString();
|
||||
productname = status.value(QStringLiteral("productname")).toString();
|
||||
product = status.value(QStringLiteral("product")).toString();
|
||||
productversion = status.value(QStringLiteral("productversion")).toString();
|
||||
}
|
||||
|
||||
QVersionNumber Status::version() const
|
||||
{
|
||||
return productversion.isEmpty() ? legacyVersion : QVersionNumber::fromString(productversion);
|
||||
}
|
||||
|
||||
QString Status::versionString() const
|
||||
{
|
||||
return productversion.isEmpty() ? legacyVersionString : productversion;
|
||||
}
|
||||
|
||||
TusSupport::TusSupport(const QVariantMap &tus_support)
|
||||
{
|
||||
if (tus_support.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
version = QVersionNumber::fromString(tus_support.value(QStringLiteral("version")).toString());
|
||||
resumable = QVersionNumber::fromString(tus_support.value(QStringLiteral("resumable")).toString());
|
||||
|
||||
extensions = tus_support.value(QStringLiteral("extension")).toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
|
||||
max_chunk_size = tus_support.value(QStringLiteral("max_chunk_size")).value<quint64>();
|
||||
http_method_override = tus_support.value(QStringLiteral("http_method_override")).toString();
|
||||
}
|
||||
|
||||
bool TusSupport::isValid() const
|
||||
{
|
||||
return !version.isNull();
|
||||
}
|
||||
|
||||
SpaceSupport::SpaceSupport(const QVariantMap &spaces_support)
|
||||
{
|
||||
if (spaces_support.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
enabled = spaces_support.value(QStringLiteral("enabled")).toBool();
|
||||
version = QVersionNumber::fromString(spaces_support.value(QStringLiteral("version")).toString());
|
||||
}
|
||||
|
||||
bool SpaceSupport::isValid() const
|
||||
{
|
||||
return !version.isNull();
|
||||
}
|
||||
|
||||
Capabilities::AppProviders::AppProviders(const QUrl &baseUrl, const QVariantMap &appProviders)
|
||||
: enabled(appProviders.value(QStringLiteral("enabled")).toBool())
|
||||
, version(QVersionNumber::fromString(appProviders.value(QStringLiteral("version")).toString()))
|
||||
, appsUrl(baseUrl.resolved(appProviders.value(QStringLiteral("apps_url")).toUrl()))
|
||||
, openUrl(baseUrl.resolved(appProviders.value(QStringLiteral("open_url")).toUrl()))
|
||||
, newUrl(baseUrl.resolved(appProviders.value(QStringLiteral("new_url")).toUrl()))
|
||||
, openWebUrl(baseUrl.resolved(appProviders.value(QStringLiteral("open_web_url")).toUrl()))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
const Capabilities::AppProviders &Capabilities::appProviders() const
|
||||
{
|
||||
return _appProviders;
|
||||
}
|
||||
|
||||
Capabilities::AppProviders Capabilities::AppProviders::findVersion(const QUrl &baseUrl, const QVariantList &list, const QVersionNumber &v)
|
||||
{
|
||||
auto it = std::find_if(list.cbegin(), list.cend(), [&v](const auto &it) {
|
||||
return QVersionNumber::fromString(it.toMap().value(QStringLiteral("version")).toString()) == v;
|
||||
});
|
||||
return it != list.cend() ? Capabilities::AppProviders{baseUrl, it->toMap()} : Capabilities::AppProviders{};
|
||||
}
|
||||
|
||||
const FilesSharing &Capabilities::filesSharing() const
|
||||
{
|
||||
return _filesSharing;
|
||||
}
|
||||
|
||||
FilesSharing::FilesSharing(const QVariantMap &filesSharing)
|
||||
: sharing_roles(filesSharing.value(QStringLiteral("sharing_roles"), false).toBool())
|
||||
{
|
||||
}
|
||||
|
||||
OCC::Migration::Migration(const QVariantMap &data)
|
||||
{
|
||||
const auto spaces = data.value(QStringLiteral("space_migration")).toMap();
|
||||
space_migration.enabled = spaces.value(QStringLiteral("enabled")).toBool();
|
||||
space_migration.endpoint = spaces.value(QStringLiteral("endpoint")).toString();
|
||||
}
|
||||
|
||||
const Migration &Capabilities::migration() const
|
||||
{
|
||||
return _migration;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
|
||||
#ifndef CAPABILITIES_H
|
||||
#define CAPABILITIES_H
|
||||
|
||||
#include "qsferasynclib.h"
|
||||
|
||||
#include "common/checksumalgorithms.h"
|
||||
|
||||
#include <QVariantMap>
|
||||
#include <QStringList>
|
||||
#include <QVersionNumber>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
struct QSFERA_SYNC_EXPORT Status
|
||||
{
|
||||
/**
|
||||
<installed>1</installed>
|
||||
<maintenance>0</maintenance>
|
||||
<needsDbUpgrade>0</needsDbUpgrade>
|
||||
<version>10.11.0.0</version>
|
||||
<versionstring>10.11.0</versionstring>
|
||||
<edition>Community</edition>
|
||||
<productname>QSfera</productname>
|
||||
<product>QSfera</product>
|
||||
<productversion>1.0.0-beta1+7c2e3201b</productversion>
|
||||
*/
|
||||
|
||||
Status(const QVariantMap &status);
|
||||
// legacy version
|
||||
QVersionNumber legacyVersion;
|
||||
// legacy version
|
||||
QString legacyVersionString;
|
||||
|
||||
QString edition;
|
||||
QString productname;
|
||||
QString product;
|
||||
QString productversion;
|
||||
|
||||
QVersionNumber version() const;
|
||||
QString versionString() const;
|
||||
};
|
||||
|
||||
struct QSFERA_SYNC_EXPORT TusSupport
|
||||
{
|
||||
/**
|
||||
<tus_support>
|
||||
<version>1.0.0</version>
|
||||
<resumable>1.0.0</resumable>
|
||||
<extension>creation,creation-with-upload</extension>
|
||||
<max_chunk_size>0</max_chunk_size>
|
||||
<http_method_override/>
|
||||
</tus_support>
|
||||
*/
|
||||
TusSupport(const QVariantMap &tus_support);
|
||||
QVersionNumber version;
|
||||
QVersionNumber resumable;
|
||||
QStringList extensions;
|
||||
quint64 max_chunk_size;
|
||||
QString http_method_override;
|
||||
|
||||
bool isValid() const;
|
||||
};
|
||||
|
||||
struct QSFERA_SYNC_EXPORT SpaceSupport
|
||||
{
|
||||
/**
|
||||
"spaces": {
|
||||
"version": "0.0.1",
|
||||
"enabled": true
|
||||
}
|
||||
*/
|
||||
SpaceSupport(const QVariantMap &spaces_support);
|
||||
bool enabled = false;
|
||||
QVersionNumber version;
|
||||
|
||||
bool isValid() const;
|
||||
};
|
||||
|
||||
struct QSFERA_SYNC_EXPORT FilesSharing
|
||||
{
|
||||
/**
|
||||
api_enabled": true,
|
||||
"resharing": true,
|
||||
"group_sharing": true,
|
||||
"sharing_roles": true,
|
||||
"auto_accept_share": true,
|
||||
"share_with_group_members_only": true,
|
||||
"share_with_membership_groups_only": true,
|
||||
"search_min_length": 3,
|
||||
"default_permissions": 22,
|
||||
*/
|
||||
FilesSharing(const QVariantMap &filesSharing);
|
||||
|
||||
// TODO: add more
|
||||
bool sharing_roles = false;
|
||||
};
|
||||
|
||||
struct QSFERA_SYNC_EXPORT Migration
|
||||
{
|
||||
struct SpacesMigration
|
||||
{
|
||||
bool enabled = false;
|
||||
QString endpoint;
|
||||
};
|
||||
|
||||
explicit Migration(const QVariantMap &spaces_support);
|
||||
|
||||
SpacesMigration space_migration;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The Capabilities class represents the capabilities of the server
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT Capabilities
|
||||
{
|
||||
public:
|
||||
struct QSFERA_SYNC_EXPORT AppProviders
|
||||
{
|
||||
/**
|
||||
"app_providers": [
|
||||
{
|
||||
"enabled": true,
|
||||
"version": "1.0.0",
|
||||
"apps_url": "/app/list",
|
||||
"open_url": "/app/open",
|
||||
"new_url": "/app/new"
|
||||
}
|
||||
*/
|
||||
AppProviders() = default;
|
||||
static AppProviders findVersion(const QUrl &baseUrl, const QVariantList &list, const QVersionNumber &v);
|
||||
AppProviders(const QUrl &baseUrl, const QVariantMap &appProviders);
|
||||
|
||||
bool enabled = false;
|
||||
QVersionNumber version;
|
||||
QUrl appsUrl;
|
||||
QUrl openUrl;
|
||||
QUrl newUrl;
|
||||
QUrl openWebUrl;
|
||||
};
|
||||
|
||||
|
||||
Capabilities(const QUrl &url, const QVariantMap &capabilities);
|
||||
|
||||
bool shareAPI() const;
|
||||
bool sharePublicLink() const;
|
||||
bool sharePublicLinkAllowUpload() const;
|
||||
bool sharePublicLinkSupportsUploadOnly() const;
|
||||
|
||||
/** Whether read-only link shares require a password.
|
||||
*
|
||||
* Returns sharePublicLinkEnforcePassword() if the fine-grained
|
||||
* permission isn't available.
|
||||
*/
|
||||
bool sharePublicLinkEnforcePasswordForReadOnly() const;
|
||||
bool sharePublicLinkEnforcePasswordForReadWrite() const;
|
||||
bool sharePublicLinkEnforcePasswordForUploadOnly() const;
|
||||
|
||||
bool sharePublicLinkDefaultExpire() const;
|
||||
int sharePublicLinkDefaultExpireDateDays() const;
|
||||
bool sharePublicLinkEnforceExpireDate() const;
|
||||
bool sharePublicLinkMultiple() const;
|
||||
bool shareResharing() const;
|
||||
/** Remote Poll interval.
|
||||
*
|
||||
* returns the requested poll interval in seconds to be used by the client.
|
||||
* @returns 0 if no capability is set.
|
||||
*/
|
||||
std::chrono::seconds remotePollInterval() const;
|
||||
|
||||
// TODO: return SharePermission
|
||||
int defaultPermissions() const;
|
||||
|
||||
const Status &status() const;
|
||||
bool checkForUpdates() const;
|
||||
const TusSupport &tusSupport() const;
|
||||
const SpaceSupport &spacesSupport() const;
|
||||
|
||||
/// Whether the "privatelink" DAV property is available
|
||||
bool privateLinkPropertyAvailable() const;
|
||||
|
||||
/// Whether the "privatelink" DAV property supports the 'details' param
|
||||
bool privateLinkDetailsParamAvailable() const;
|
||||
|
||||
/// returns true if the capabilities report notifications
|
||||
bool notificationsAvailable() const;
|
||||
|
||||
/// returns true if the capabilities are loaded already.
|
||||
bool isValid() const;
|
||||
|
||||
/**
|
||||
* Returns the checksum types the server understands.
|
||||
*
|
||||
* When the client uses one of these checksumming algorithms in
|
||||
* the OC-Checksum header of a file upload, the server will use
|
||||
* it to validate that data was transmitted correctly.
|
||||
*
|
||||
* Path: checksums/supportedTypes
|
||||
* Default: []
|
||||
* Possible entries: "Adler32", "MD5", "SHA1"
|
||||
*/
|
||||
QList<OCC::CheckSums::Algorithm> supportedChecksumTypes() const;
|
||||
|
||||
/**
|
||||
* The checksum algorithm that the server recommends for file uploads.
|
||||
* This is just a preference, any algorithm listed in supportedTypes may be used.
|
||||
*
|
||||
* Path: checksums/preferredUploadType
|
||||
*/
|
||||
OCC::CheckSums::Algorithm preferredUploadChecksumType() const;
|
||||
|
||||
/**
|
||||
* Helper that returns the preferredUploadChecksumType() if set, or one
|
||||
* of the supportedChecksumTypes() if it isn't.
|
||||
*/
|
||||
CheckSums::Algorithm uploadChecksumType() const;
|
||||
|
||||
/**
|
||||
* List of HTTP error codes should be guaranteed to eventually reset
|
||||
* failing chunked uploads.
|
||||
*
|
||||
* The resetting works by tracking UploadInfo::errorCount.
|
||||
*
|
||||
* Note that other error codes than the ones listed here may reset the
|
||||
* upload as well.
|
||||
*
|
||||
* Motivation: See #5344. They should always be reset on 412 (possibly
|
||||
* checksum error), but broken servers may also require resets on
|
||||
* unusual error codes such as 503.
|
||||
*
|
||||
* Path: dav/httpErrorCodesThatResetFailingChunkedUploads
|
||||
* Default: []
|
||||
* Example: [503, 500]
|
||||
*/
|
||||
QList<int> httpErrorCodesThatResetFailingChunkedUploads() const;
|
||||
|
||||
/**
|
||||
* Regex that, if contained in a filename, will result in it not being uploaded.
|
||||
*
|
||||
* For servers older than 8.1.0 it defaults to [\\:?*"<>|]
|
||||
* For servers >= that version, it defaults to the empty regex (the server
|
||||
* will indicate invalid characters through an upload error)
|
||||
*
|
||||
* Note that it just needs to be contained. The regex [ab] is contained in "car".
|
||||
*/
|
||||
QString invalidFilenameRegex() const;
|
||||
|
||||
/**
|
||||
* return the list of filename that should not be uploaded
|
||||
*/
|
||||
QStringList blacklistedFiles() const;
|
||||
|
||||
/**
|
||||
* Whether conflict files should remain local (default) or should be uploaded.
|
||||
*/
|
||||
bool uploadConflictFiles() const;
|
||||
|
||||
/** Is versioning available? */
|
||||
bool versioningEnabled() const;
|
||||
|
||||
const AppProviders &appProviders() const;
|
||||
|
||||
const FilesSharing &filesSharing() const;
|
||||
|
||||
const Migration &migration() const;
|
||||
|
||||
QVariantMap raw() const;
|
||||
|
||||
bool operator==(const Capabilities &other) const;
|
||||
bool operator!=(const Capabilities &other) const { return !(*this == other); }
|
||||
|
||||
private:
|
||||
QVariantMap _capabilities;
|
||||
QVariantMap _fileSharingCapabilities;
|
||||
QVariantMap _fileSharingPublicCapabilities;
|
||||
TusSupport _tusSupport;
|
||||
SpaceSupport _spaces;
|
||||
Status _status;
|
||||
bool _checkForUpdates;
|
||||
AppProviders _appProviders;
|
||||
FilesSharing _filesSharing;
|
||||
Migration _migration;
|
||||
};
|
||||
}
|
||||
|
||||
#endif //CAPABILITIES_H
|
||||
@@ -0,0 +1,33 @@
|
||||
configure_file(version.cpp.in ${CMAKE_CURRENT_BINARY_DIR}/version.cpp @ONLY)
|
||||
|
||||
target_sources(libsync PRIVATE
|
||||
checksums.cpp
|
||||
checksumalgorithms.cpp
|
||||
chronoelapsedtimer.cpp
|
||||
filesystembase.cpp
|
||||
ownsql.cpp
|
||||
preparedsqlquerymanager.cpp
|
||||
syncjournaldb.cpp
|
||||
syncjournalfilerecord.cpp
|
||||
utility.cpp
|
||||
remotepermissions.cpp
|
||||
pinstate.cpp
|
||||
plugin.cpp
|
||||
restartmanager.cpp
|
||||
syncfilestatus.cpp
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_sources(libsync PRIVATE
|
||||
utility_win.cpp
|
||||
)
|
||||
elseif(APPLE)
|
||||
target_sources(libsync PRIVATE
|
||||
utility_mac.mm
|
||||
)
|
||||
elseif(UNIX AND NOT APPLE)
|
||||
target_sources(libsync PRIVATE
|
||||
utility_unix.cpp
|
||||
)
|
||||
endif()
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <qglobal.h>
|
||||
|
||||
#if defined(QT_FORCE_ASSERTS) || !defined(QT_NO_DEBUG)
|
||||
#define OC_ASSERT_MSG qFatal
|
||||
#else
|
||||
#define OC_ASSERT_MSG qCritical
|
||||
#endif
|
||||
|
||||
// Default assert: If the condition is false in debug builds, terminate.
|
||||
//
|
||||
// Prints a message on failure, even in release builds.
|
||||
#define OC_ASSERT(cond) \
|
||||
if (!(cond)) { \
|
||||
OC_ASSERT_MSG("ASSERT: \"%s\" in file %s, line %d %s", #cond, __FILE__, __LINE__, Q_FUNC_INFO); \
|
||||
} else { \
|
||||
}
|
||||
#define OC_ASSERT_X(cond, message) \
|
||||
if (!(cond)) { \
|
||||
OC_ASSERT_MSG("ASSERT: \"%s\" in file %s, line %d %s with message: %s", #cond, __FILE__, __LINE__, Q_FUNC_INFO, message); \
|
||||
} else { \
|
||||
}
|
||||
|
||||
// Enforce condition to be true, even in release builds.
|
||||
//
|
||||
// Prints 'message' and aborts execution if 'cond' is false.
|
||||
#define OC_ENFORCE(cond) \
|
||||
if (!(cond)) { \
|
||||
qFatal("ENFORCE: \"%s\" in file %s, line %d %s", #cond, __FILE__, __LINE__, Q_FUNC_INFO); \
|
||||
} else { \
|
||||
}
|
||||
#define OC_ENFORCE_X(cond, message) \
|
||||
if (!(cond)) { \
|
||||
qFatal("ENFORCE: \"%s\" in file %s, line %d %s with message: %s", #cond, __FILE__, __LINE__, Q_FUNC_INFO, message); \
|
||||
} else { \
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool __OC_ENSURE(bool condition, const char *cond, const char *file, int line, const char *info)
|
||||
{
|
||||
if (Q_UNLIKELY(!condition)) {
|
||||
OC_ASSERT_MSG("ENSURE: \"%s\" in file %s, line %d %s", cond, file, line, info);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#define OC_ENSURE(cond) Q_LIKELY(__OC_ENSURE(cond, #cond, __FILE__, __LINE__, Q_FUNC_INFO))
|
||||
#define OC_ENSURE_NOT(cond) OC_ENSURE(!cond)
|
||||
// An assert that is only present in debug builds: typically used for
|
||||
// asserts that are too expensive for release mode.
|
||||
//
|
||||
// Q_ASSERT
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* c_jhash.c Jenkins Hash
|
||||
*
|
||||
* Copyright (c) 1997 Bob Jenkins <bob_jenkins@burtleburtle.net>
|
||||
*
|
||||
* lookup8.c, by Bob Jenkins, January 4 1997, Public Domain.
|
||||
* hash(), hash2(), hash3, and _c_mix() are externally useful functions.
|
||||
* Routines to test the hash are included if SELF_TEST is defined.
|
||||
* You can use this free for any purpose. It has no warranty.
|
||||
*
|
||||
* See http://burtleburtle.net/bob/hash/evahash.html
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file common/c_jhash.h
|
||||
*
|
||||
* @brief Interface of the cynapses jhash implementation
|
||||
*
|
||||
* @defgroup cynJHashInternals cynapses libc jhash function
|
||||
* @ingroup cynLibraryAPI
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
#ifndef _C_JHASH_H
|
||||
#define _C_JHASH_H
|
||||
|
||||
#include <QtCore/qglobal.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* _c_mix64 -- Mix 3 64-bit values reversibly.
|
||||
*
|
||||
* _c_mix64() takes 48 machine instructions, but only 24 cycles on a superscalar
|
||||
* machine (like Intel's new MMX architecture). It requires 4 64-bit
|
||||
* registers for 4::2 parallelism.
|
||||
* All 1-bit deltas, all 2-bit deltas, all deltas composed of top bits of
|
||||
* (a,b,c), and all deltas of bottom bits were tested. All deltas were
|
||||
* tested both on random keys and on keys that were nearly all zero.
|
||||
* These deltas all cause every bit of c to change between 1/3 and 2/3
|
||||
* of the time (well, only 113/400 to 287/400 of the time for some
|
||||
* 2-bit delta). These deltas all cause at least 80 bits to change
|
||||
* among (a,b,c) when the _c_mix is run either forward or backward (yes it
|
||||
* is reversible).
|
||||
* This implies that a hash using _c_mix64 has no funnels. There may be
|
||||
* characteristics with 3-bit deltas or bigger, I didn't test for
|
||||
* those.
|
||||
*/
|
||||
#define _c_mix64(a, b, c) \
|
||||
{ \
|
||||
a -= b; \
|
||||
a -= c; \
|
||||
a ^= (c >> 43); \
|
||||
b -= c; \
|
||||
b -= a; \
|
||||
b ^= (a << 9); \
|
||||
c -= a; \
|
||||
c -= b; \
|
||||
c ^= (b >> 8); \
|
||||
a -= b; \
|
||||
a -= c; \
|
||||
a ^= (c >> 38); \
|
||||
b -= c; \
|
||||
b -= a; \
|
||||
b ^= (a << 23); \
|
||||
c -= a; \
|
||||
c -= b; \
|
||||
c ^= (b >> 5); \
|
||||
a -= b; \
|
||||
a -= c; \
|
||||
a ^= (c >> 35); \
|
||||
b -= c; \
|
||||
b -= a; \
|
||||
b ^= (a << 49); \
|
||||
c -= a; \
|
||||
c -= b; \
|
||||
c ^= (b >> 11); \
|
||||
a -= b; \
|
||||
a -= c; \
|
||||
a ^= (c >> 12); \
|
||||
b -= c; \
|
||||
b -= a; \
|
||||
b ^= (a << 18); \
|
||||
c -= a; \
|
||||
c -= b; \
|
||||
c ^= (b >> 22); \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief hash a variable-length key into a 64-bit value
|
||||
*
|
||||
* The best hash table sizes are powers of 2. There is no need to do
|
||||
* mod a prime (mod is sooo slow!). If you need less than 64 bits,
|
||||
* use a bitmask. For example, if you need only 10 bits, do
|
||||
* h = (h & hashmask(10));
|
||||
* In which case, the hash table should have hashsize(10) elements.
|
||||
*
|
||||
* Use for hash table lookup, or anything where one collision in 2^^64
|
||||
* is acceptable. Do NOT use for cryptographic purposes.
|
||||
*
|
||||
* @param k The key (the unaligned variable-length array of bytes).
|
||||
* @param length The length of the key, counting by bytes.
|
||||
* @param intval Initial value, can be any 8-byte value.
|
||||
*
|
||||
* @return A 64-bit value. Every bit of the key affects every bit of
|
||||
* the return value. No funnels. Every 1-bit and 2-bit delta
|
||||
* achieves avalanche. About 41+5len instructions.
|
||||
*/
|
||||
static inline uint64_t c_jhash64(const uint8_t *k, uint64_t length, uint64_t intval)
|
||||
{
|
||||
uint64_t a, b, c, len;
|
||||
|
||||
/* Set up the internal state */
|
||||
len = length;
|
||||
a = b = intval; /* the previous hash value */
|
||||
c = 0x9e3779b97f4a7c13LL; /* the golden ratio; an arbitrary value */
|
||||
|
||||
/* handle most of the key */
|
||||
while (len >= 24) {
|
||||
a += (k[0] + ((uint64_t)k[1] << 8) + ((uint64_t)k[2] << 16) + ((uint64_t)k[3] << 24) + ((uint64_t)k[4] << 32) + ((uint64_t)k[5] << 40)
|
||||
+ ((uint64_t)k[6] << 48) + ((uint64_t)k[7] << 56));
|
||||
b += (k[8] + ((uint64_t)k[9] << 8) + ((uint64_t)k[10] << 16) + ((uint64_t)k[11] << 24) + ((uint64_t)k[12] << 32) + ((uint64_t)k[13] << 40)
|
||||
+ ((uint64_t)k[14] << 48) + ((uint64_t)k[15] << 56));
|
||||
c += (k[16] + ((uint64_t)k[17] << 8) + ((uint64_t)k[18] << 16) + ((uint64_t)k[19] << 24) + ((uint64_t)k[20] << 32) + ((uint64_t)k[21] << 40)
|
||||
+ ((uint64_t)k[22] << 48) + ((uint64_t)k[23] << 56));
|
||||
_c_mix64(a, b, c);
|
||||
k += 24;
|
||||
len -= 24;
|
||||
}
|
||||
|
||||
/* handle the last 23 bytes */
|
||||
c += length;
|
||||
switch (len) {
|
||||
case 23:
|
||||
c += ((uint64_t)k[22] << 56);
|
||||
Q_FALLTHROUGH();
|
||||
case 22:
|
||||
c += ((uint64_t)k[21] << 48);
|
||||
Q_FALLTHROUGH();
|
||||
case 21:
|
||||
c += ((uint64_t)k[20] << 40);
|
||||
Q_FALLTHROUGH();
|
||||
case 20:
|
||||
c += ((uint64_t)k[19] << 32);
|
||||
Q_FALLTHROUGH();
|
||||
case 19:
|
||||
c += ((uint64_t)k[18] << 24);
|
||||
Q_FALLTHROUGH();
|
||||
case 18:
|
||||
c += ((uint64_t)k[17] << 16);
|
||||
Q_FALLTHROUGH();
|
||||
case 17:
|
||||
c += ((uint64_t)k[16] << 8);
|
||||
Q_FALLTHROUGH();
|
||||
/* the first byte of c is reserved for the length */
|
||||
case 16:
|
||||
b += ((uint64_t)k[15] << 56);
|
||||
Q_FALLTHROUGH();
|
||||
case 15:
|
||||
b += ((uint64_t)k[14] << 48);
|
||||
Q_FALLTHROUGH();
|
||||
case 14:
|
||||
b += ((uint64_t)k[13] << 40);
|
||||
Q_FALLTHROUGH();
|
||||
case 13:
|
||||
b += ((uint64_t)k[12] << 32);
|
||||
Q_FALLTHROUGH();
|
||||
case 12:
|
||||
b += ((uint64_t)k[11] << 24);
|
||||
Q_FALLTHROUGH();
|
||||
case 11:
|
||||
b += ((uint64_t)k[10] << 16);
|
||||
Q_FALLTHROUGH();
|
||||
case 10:
|
||||
b += ((uint64_t)k[9] << 8);
|
||||
Q_FALLTHROUGH();
|
||||
case 9:
|
||||
b += ((uint64_t)k[8]);
|
||||
Q_FALLTHROUGH();
|
||||
case 8:
|
||||
a += ((uint64_t)k[7] << 56);
|
||||
Q_FALLTHROUGH();
|
||||
case 7:
|
||||
a += ((uint64_t)k[6] << 48);
|
||||
Q_FALLTHROUGH();
|
||||
case 6:
|
||||
a += ((uint64_t)k[5] << 40);
|
||||
Q_FALLTHROUGH();
|
||||
case 5:
|
||||
a += ((uint64_t)k[4] << 32);
|
||||
Q_FALLTHROUGH();
|
||||
case 4:
|
||||
a += ((uint64_t)k[3] << 24);
|
||||
Q_FALLTHROUGH();
|
||||
case 3:
|
||||
a += ((uint64_t)k[2] << 16);
|
||||
Q_FALLTHROUGH();
|
||||
case 2:
|
||||
a += ((uint64_t)k[1] << 8);
|
||||
Q_FALLTHROUGH();
|
||||
case 1:
|
||||
a += ((uint64_t)k[0]);
|
||||
/* case 0: nothing left to add */
|
||||
}
|
||||
_c_mix64(a, b, c);
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* }@
|
||||
*/
|
||||
#endif /* _C_JHASH_H */
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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 "checksumalgorithms.h"
|
||||
|
||||
using namespace OCC;
|
||||
CheckSums::Algorithm CheckSums::fromByteArray(const QByteArray &s)
|
||||
{
|
||||
// assert to ensure that all keys are uppercase
|
||||
Q_ASSERT([] {
|
||||
// ensure to run the check only once
|
||||
static bool once = [] {
|
||||
for (const auto &a : All) {
|
||||
const QString s = Utility::enumToString(a.first);
|
||||
if (s != s.toUpper()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
return once;
|
||||
}());
|
||||
return fromName(s.toUpper().constData());
|
||||
}
|
||||
|
||||
template <>
|
||||
QString Utility::enumToString(CheckSums::Algorithm algo)
|
||||
{
|
||||
const auto n = toString(algo);
|
||||
return QString::fromUtf8(n.data(), static_cast<int>(n.size()));
|
||||
}
|
||||
|
||||
#include "moc_checksumalgorithms.cpp"
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "constexpr_list.h"
|
||||
#include "utility.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QString>
|
||||
|
||||
|
||||
namespace OCC {
|
||||
namespace CheckSums {
|
||||
QSFERA_SYNC_EXPORT Q_NAMESPACE;
|
||||
|
||||
enum class Algorithm {
|
||||
NONE,
|
||||
SHA3_256 = QCryptographicHash::Sha3_256,
|
||||
SHA256 = QCryptographicHash::Sha256,
|
||||
SHA1 = QCryptographicHash::Sha1,
|
||||
MD5 = QCryptographicHash::Md5,
|
||||
ADLER32 = 100,
|
||||
DUMMY_FOR_TESTS,
|
||||
PARSE_ERROR
|
||||
};
|
||||
Q_ENUM_NS(Algorithm);
|
||||
|
||||
constexpr std::string_view toString(Algorithm algo)
|
||||
{
|
||||
switch (algo) {
|
||||
case Algorithm::SHA3_256:
|
||||
return "SHA3-256";
|
||||
case Algorithm::SHA256:
|
||||
return "SHA256";
|
||||
case Algorithm::SHA1:
|
||||
return "SHA1";
|
||||
case Algorithm::MD5:
|
||||
return "MD5";
|
||||
case Algorithm::ADLER32:
|
||||
return "ADLER32";
|
||||
case Algorithm::DUMMY_FOR_TESTS:
|
||||
return "DUMMY_FOR_TESTS";
|
||||
case Algorithm::PARSE_ERROR:
|
||||
break;
|
||||
case Algorithm::NONE:
|
||||
// while none is a valid enum value, it is not valid when used to create a checksum string
|
||||
Q_UNREACHABLE();
|
||||
break;
|
||||
}
|
||||
return "ERROR";
|
||||
}
|
||||
|
||||
inline QString toQString(Algorithm algo)
|
||||
{
|
||||
const auto n = toString(algo);
|
||||
return QString::fromUtf8(n.data(), n.size());
|
||||
}
|
||||
|
||||
constexpr auto pair(Algorithm a)
|
||||
{
|
||||
return std::make_pair(a, toString(a));
|
||||
}
|
||||
|
||||
constexpr_list auto All = {
|
||||
// Sorted by priority
|
||||
pair(Algorithm::SHA3_256), pair(Algorithm::SHA256), pair(Algorithm::SHA1), pair(Algorithm::MD5), pair(Algorithm::ADLER32),
|
||||
pair(Algorithm::DUMMY_FOR_TESTS)};
|
||||
|
||||
constexpr_list auto UnsafeAlgorithms = {pair(Algorithm::ADLER32), pair(Algorithm::DUMMY_FOR_TESTS)};
|
||||
|
||||
constexpr_list auto SafeAlgorithms = {pair(Algorithm::SHA3_256), pair(Algorithm::SHA256), pair(Algorithm::SHA1), pair(Algorithm::MD5)};
|
||||
|
||||
inline Algorithm fromName(std::string_view s)
|
||||
{
|
||||
auto it = std::find_if(All.begin(), All.end(), [s](const auto &it) { return it.second == s; });
|
||||
if (it != All.end()) {
|
||||
return it->first;
|
||||
}
|
||||
return Algorithm::PARSE_ERROR;
|
||||
}
|
||||
|
||||
QSFERA_SYNC_EXPORT Algorithm fromByteArray(const QByteArray &s);
|
||||
} // namespace CheckSums
|
||||
|
||||
template <>
|
||||
QSFERA_SYNC_EXPORT QString Utility::enumToString(CheckSums::Algorithm algo);
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* 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 "common/checksums.h"
|
||||
#include "asserts.h"
|
||||
#include "common/chronoelapsedtimer.h"
|
||||
#include "common/utility.h"
|
||||
#include "filesystembase.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QLoggingCategory>
|
||||
#include <QScopeGuard>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
/** \file checksums.cpp
|
||||
*
|
||||
* \brief Computing and validating file checksums
|
||||
*
|
||||
* Overview
|
||||
* --------
|
||||
*
|
||||
* Checksums are used in two distinct ways during synchronization:
|
||||
*
|
||||
* - to guard uploads and downloads against data corruption
|
||||
* (transmission checksum)
|
||||
* - to quickly check whether the content of a file has changed
|
||||
* to avoid redundant uploads (content checksum)
|
||||
*
|
||||
* In principle both are independent and different checksumming
|
||||
* algorithms can be used. To avoid redundant computations, it can
|
||||
* make sense to use the same checksum algorithm though.
|
||||
*
|
||||
* Transmission Checksums
|
||||
* ----------------------
|
||||
*
|
||||
* The usage of transmission checksums is currently optional and needs
|
||||
* to be explicitly enabled by adding 'transmissionChecksum=TYPE' to
|
||||
* the '[General]' section of the config file.
|
||||
*
|
||||
* When enabled, the checksum will be calculated on upload and sent to
|
||||
* the server in the OC-Checksum header with the format 'TYPE:CHECKSUM'.
|
||||
*
|
||||
* On download, the header with the same name is read and if the
|
||||
* received data does not have the expected checksum, the download is
|
||||
* rejected.
|
||||
*
|
||||
* Transmission checksums guard a specific sync action and are not stored
|
||||
* in the database.
|
||||
*
|
||||
* Content Checksums
|
||||
* -----------------
|
||||
*
|
||||
* Sometimes the metadata of a local file changes while the content stays
|
||||
* unchanged. Content checksums allow the sync client to avoid uploading
|
||||
* the same data again by comparing the file's actual checksum to the
|
||||
* checksum stored in the database.
|
||||
*
|
||||
* Content checksums are not sent to the server.
|
||||
*
|
||||
* Checksum Algorithms
|
||||
* -------------------
|
||||
*
|
||||
* - Adler32 (requires zlib)
|
||||
* - MD5
|
||||
* - SHA1
|
||||
* - SHA256
|
||||
* - SHA3-256 (requires Qt 5.9)
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
QByteArray calcAdler32(QIODevice *device)
|
||||
{
|
||||
const qint64 BUFSIZE(500 * 1024); // 500 KiB
|
||||
if (device->size() == 0) {
|
||||
return QByteArray();
|
||||
}
|
||||
QByteArray buf(BUFSIZE, Qt::Uninitialized);
|
||||
|
||||
unsigned int adler = adler32(0L, nullptr, 0);
|
||||
qint64 size;
|
||||
while (!device->atEnd()) {
|
||||
size = device->read(buf.data(), BUFSIZE);
|
||||
if (size > 0)
|
||||
adler = adler32(adler, (const Bytef *)buf.data(), size);
|
||||
}
|
||||
|
||||
return QByteArray::number(adler, 16);
|
||||
}
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcChecksums, "sync.checksums", QtInfoMsg)
|
||||
Q_LOGGING_CATEGORY(lcChecksumsHeader, "sync.checksums.header", QtInfoMsg)
|
||||
|
||||
ChecksumHeader ChecksumHeader::parseChecksumHeader(const QByteArray &header)
|
||||
{
|
||||
if (header.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
ChecksumHeader out;
|
||||
const auto idx = header.indexOf(':');
|
||||
if (idx < 0) {
|
||||
out._error = QCoreApplication::translate("ChecksumHeader", "The checksum header is malformed: %1").arg(QString::fromUtf8(header));
|
||||
} else {
|
||||
out._checksumType = CheckSums::fromByteArray(header.left(idx));
|
||||
if (out._checksumType == CheckSums::Algorithm::PARSE_ERROR) {
|
||||
out._error = QCoreApplication::translate("ChecksumHeader", "The checksum header contained an unknown checksum type '%1'")
|
||||
.arg(QString::fromUtf8(header.left(idx)));
|
||||
}
|
||||
out._checksum = header.mid(idx + 1);
|
||||
}
|
||||
if (!out.isValid()) {
|
||||
qCDebug(lcChecksumsHeader) << u"Failed to parse" << header << out.error();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
ChecksumHeader::ChecksumHeader(CheckSums::Algorithm type, const QByteArray &checksum)
|
||||
: _checksumType(type)
|
||||
, _checksum(checksum)
|
||||
{
|
||||
}
|
||||
|
||||
QByteArray ChecksumHeader::makeChecksumHeader() const
|
||||
{
|
||||
if (!isValid()) {
|
||||
return {};
|
||||
}
|
||||
return Utility::enumToString(_checksumType).toUtf8() + ':' + _checksum;
|
||||
}
|
||||
|
||||
CheckSums::Algorithm ChecksumHeader::type() const
|
||||
{
|
||||
return _checksumType;
|
||||
}
|
||||
|
||||
QByteArray ChecksumHeader::checksum() const
|
||||
{
|
||||
return _checksum;
|
||||
}
|
||||
|
||||
bool ChecksumHeader::isValid() const
|
||||
{
|
||||
return _checksumType != CheckSums::Algorithm::NONE && _checksumType != CheckSums::Algorithm::PARSE_ERROR && !_checksum.isEmpty();
|
||||
}
|
||||
|
||||
QString ChecksumHeader::error() const
|
||||
{
|
||||
return _error;
|
||||
}
|
||||
|
||||
bool ChecksumHeader::operator==(const ChecksumHeader &other) const
|
||||
{
|
||||
return _checksumType == other._checksumType && _checksum == other._checksum;
|
||||
}
|
||||
|
||||
bool ChecksumHeader::operator!=(const ChecksumHeader &other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
QByteArray findBestChecksum(const QByteArray &_checksums)
|
||||
{
|
||||
if (_checksums.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// The order of the searches here defines the preference ordering.
|
||||
// we assume _checksums to be utf-8 so toUpper is valid here
|
||||
const auto i = [checksums = _checksums.toUpper()]() -> qsizetype {
|
||||
for (const auto &algo : CheckSums::All) {
|
||||
auto i = checksums.indexOf(algo.second.data());
|
||||
if (i != -1) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}();
|
||||
if (i != -1) {
|
||||
// Now i is the start of the best checksum
|
||||
// Grab it until the next space or end of xml or end of string.
|
||||
int end = _checksums.indexOf(' ', i);
|
||||
// workaround for https://github.com/owncloud/core/pull/38304
|
||||
if (end == -1) {
|
||||
end = _checksums.indexOf('<', i);
|
||||
}
|
||||
return _checksums.mid(i, end - i);
|
||||
}
|
||||
qCWarning(lcChecksums) << u"Failed to parse" << _checksums;
|
||||
return {};
|
||||
}
|
||||
|
||||
ComputeChecksum::ComputeChecksum(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
ComputeChecksum::~ComputeChecksum() { }
|
||||
|
||||
void ComputeChecksum::setChecksumType(CheckSums::Algorithm type)
|
||||
{
|
||||
Q_ASSERT(type != CheckSums::Algorithm::NONE && type != CheckSums::Algorithm::PARSE_ERROR);
|
||||
_checksumType = type;
|
||||
}
|
||||
|
||||
CheckSums::Algorithm ComputeChecksum::checksumType() const
|
||||
{
|
||||
return _checksumType;
|
||||
}
|
||||
|
||||
void ComputeChecksum::start(const QString &filePath)
|
||||
{
|
||||
qCInfo(lcChecksums) << u"Computing" << checksumType() << u"checksum of" << filePath << u"in a thread";
|
||||
startImpl(std::make_unique<QFile>(filePath));
|
||||
}
|
||||
|
||||
void ComputeChecksum::start(std::unique_ptr<QIODevice> device)
|
||||
{
|
||||
OC_ENFORCE(device);
|
||||
qCInfo(lcChecksums) << u"Computing" << checksumType() << u"checksum of device" << device.get() << u"in a thread";
|
||||
OC_ASSERT(!device->parent());
|
||||
|
||||
startImpl(std::move(device));
|
||||
}
|
||||
|
||||
void ComputeChecksum::startImpl(std::unique_ptr<QIODevice> device)
|
||||
{
|
||||
connect(&_watcher, &QFutureWatcherBase::finished, this, &ComputeChecksum::slotCalculationDone, Qt::UniqueConnection);
|
||||
|
||||
// We'd prefer to move the unique_ptr into the lambda, but that's
|
||||
// awkward with the C++ standard we're on
|
||||
auto sharedDevice = QSharedPointer<QIODevice>(device.release());
|
||||
|
||||
// Bug: The thread will keep running even if ComputeChecksum is deleted.
|
||||
auto type = checksumType();
|
||||
_watcher.setFuture(QtConcurrent::run([sharedDevice, type]() {
|
||||
if (!sharedDevice->open(QIODevice::ReadOnly)) {
|
||||
if (auto file = qobject_cast<QFile *>(sharedDevice.data())) {
|
||||
qCWarning(lcChecksums) << u"Could not open file" << file->fileName() << u"for reading to compute a checksum" << file->errorString();
|
||||
} else {
|
||||
qCWarning(lcChecksums) << u"Could not open device" << sharedDevice.data() << u"for reading to compute a checksum"
|
||||
<< sharedDevice->errorString();
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
auto result = ComputeChecksum::computeNow(sharedDevice.data(), type);
|
||||
sharedDevice->close();
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
|
||||
QByteArray ComputeChecksum::computeNowOnFile(const QString &filePath, CheckSums::Algorithm checksumType)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qCWarning(lcChecksums) << u"Could not open file" << filePath << u"for reading and computing checksum" << file.errorString();
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return computeNow(&file, checksumType);
|
||||
}
|
||||
|
||||
QByteArray ComputeChecksum::computeNow(QIODevice *device, CheckSums::Algorithm algorithm)
|
||||
{
|
||||
// const cast to prevent stream to "device"
|
||||
const auto log = qScopeGuard([device, algorithm, timer = Utility::ChronoElapsedTimer()] {
|
||||
if (auto file = qobject_cast<QFile *>(device)) {
|
||||
qCDebug(lcChecksums) << u"Finished" << algorithm << u"computation for" << file->fileName() << timer;
|
||||
} else {
|
||||
qCDebug(lcChecksums) << u"Finished" << algorithm << u"computation for" << device << timer;
|
||||
}
|
||||
});
|
||||
switch (algorithm) {
|
||||
case CheckSums::Algorithm::SHA3_256:
|
||||
[[fallthrough]];
|
||||
case CheckSums::Algorithm::SHA256:
|
||||
[[fallthrough]];
|
||||
case CheckSums::Algorithm::SHA1:
|
||||
[[fallthrough]];
|
||||
case CheckSums::Algorithm::MD5: {
|
||||
QCryptographicHash crypto(static_cast<QCryptographicHash::Algorithm>(algorithm));
|
||||
if (crypto.addData(device)) {
|
||||
return crypto.result().toHex();
|
||||
}
|
||||
qCWarning(lcChecksums) << u"Failed to compoute checksum" << Utility::enumToString(algorithm);
|
||||
return {};
|
||||
}
|
||||
case CheckSums::Algorithm::ADLER32:
|
||||
return calcAdler32(device);
|
||||
case CheckSums::Algorithm::DUMMY_FOR_TESTS:
|
||||
return QByteArrayLiteral("0x1");
|
||||
case CheckSums::Algorithm::PARSE_ERROR:
|
||||
return {};
|
||||
case CheckSums::Algorithm::NONE:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
||||
void ComputeChecksum::slotCalculationDone()
|
||||
{
|
||||
QByteArray checksum = _watcher.future().result();
|
||||
if (!checksum.isNull()) {
|
||||
Q_EMIT done(_checksumType, checksum);
|
||||
} else {
|
||||
Q_EMIT done(CheckSums::Algorithm::PARSE_ERROR, QByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ValidateChecksumHeader::ValidateChecksumHeader(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
ComputeChecksum *ValidateChecksumHeader::prepareStart(const QByteArray &checksumHeader)
|
||||
{
|
||||
// If the incoming header is empty no validation can happen. Just continue.
|
||||
if (checksumHeader.isEmpty()) {
|
||||
Q_EMIT validated(CheckSums::Algorithm::PARSE_ERROR, QByteArray());
|
||||
return nullptr;
|
||||
}
|
||||
_expectedChecksum = ChecksumHeader::parseChecksumHeader(checksumHeader);
|
||||
if (!_expectedChecksum.isValid()) {
|
||||
qCWarning(lcChecksums) << u"Checksum header malformed:" << checksumHeader;
|
||||
Q_EMIT validationFailed(_expectedChecksum.error());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto calculator = new ComputeChecksum(this);
|
||||
calculator->setChecksumType(_expectedChecksum.type());
|
||||
connect(calculator, &ComputeChecksum::done, this, &ValidateChecksumHeader::slotChecksumCalculated);
|
||||
return calculator;
|
||||
}
|
||||
|
||||
void ValidateChecksumHeader::start(const QString &filePath, const QByteArray &checksumHeader)
|
||||
{
|
||||
if (auto calculator = prepareStart(checksumHeader))
|
||||
calculator->start(filePath);
|
||||
}
|
||||
|
||||
void ValidateChecksumHeader::start(std::unique_ptr<QIODevice> device, const QByteArray &checksumHeader)
|
||||
{
|
||||
if (auto calculator = prepareStart(checksumHeader))
|
||||
calculator->start(std::move(device));
|
||||
}
|
||||
|
||||
void ValidateChecksumHeader::slotChecksumCalculated(CheckSums::Algorithm checksumType, const QByteArray &checksum)
|
||||
{
|
||||
if (_expectedChecksum.type() == CheckSums::Algorithm::PARSE_ERROR) {
|
||||
Q_EMIT validationFailed(_expectedChecksum.error());
|
||||
return;
|
||||
}
|
||||
if (checksum != _expectedChecksum.checksum()) {
|
||||
Q_EMIT validationFailed(tr("The downloaded file does not match the checksum, it will be resumed. '%1' != '%2'")
|
||||
.arg(QString::fromUtf8(_expectedChecksum.checksum()), QString::fromUtf8(checksum)));
|
||||
return;
|
||||
}
|
||||
Q_EMIT validated(checksumType, checksum);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "checksumalgorithms.h"
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCryptographicHash>
|
||||
#include <QFutureWatcher>
|
||||
#include <QObject>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class QFile;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* Tags for checksum headers values.
|
||||
* They are here for being shared between Upload- and Download Job
|
||||
*/
|
||||
|
||||
class SyncJournalDb;
|
||||
|
||||
/**
|
||||
* Returns the highest-quality checksum in a 'checksums'
|
||||
* property retrieved from the server.
|
||||
*
|
||||
* Example: "ADLER32:1231 SHA1:ab124124 MD5:2131affa21"
|
||||
* -> "SHA1:ab124124"
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT QByteArray findBestChecksum(const QByteArray &checksums);
|
||||
|
||||
class QSFERA_SYNC_EXPORT ChecksumHeader
|
||||
{
|
||||
public:
|
||||
static ChecksumHeader parseChecksumHeader(const QByteArray &header);
|
||||
|
||||
ChecksumHeader() = default;
|
||||
ChecksumHeader(CheckSums::Algorithm type, const QByteArray &checksum);
|
||||
|
||||
QByteArray makeChecksumHeader() const;
|
||||
|
||||
|
||||
CheckSums::Algorithm type() const;
|
||||
|
||||
QByteArray checksum() const;
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
QString error() const;
|
||||
|
||||
bool operator==(const ChecksumHeader &other) const;
|
||||
|
||||
bool operator!=(const ChecksumHeader &other) const;
|
||||
|
||||
private:
|
||||
CheckSums::Algorithm _checksumType = CheckSums::Algorithm::NONE;
|
||||
QByteArray _checksum;
|
||||
QString _error;
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the checksum of a file.
|
||||
* \ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT ComputeChecksum : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ComputeChecksum(QObject *parent = nullptr);
|
||||
~ComputeChecksum() override;
|
||||
|
||||
/**
|
||||
* Sets the checksum type to be used.
|
||||
*/
|
||||
void setChecksumType(CheckSums::Algorithm type);
|
||||
|
||||
CheckSums::Algorithm checksumType() const;
|
||||
|
||||
/**
|
||||
* Computes the checksum for the given file path.
|
||||
*
|
||||
* done() is emitted when the calculation finishes.
|
||||
*/
|
||||
void start(const QString &filePath);
|
||||
|
||||
/**
|
||||
* Computes the checksum for the given device.
|
||||
*
|
||||
* done() is emitted when the calculation finishes.
|
||||
*
|
||||
* The device ownership transfers into the thread that
|
||||
* will compute the checksum. It must not have a parent.
|
||||
*/
|
||||
void start(std::unique_ptr<QIODevice> device);
|
||||
|
||||
/**
|
||||
* Computes the checksum synchronously.
|
||||
*/
|
||||
static QByteArray computeNow(QIODevice *device, CheckSums::Algorithm algo);
|
||||
|
||||
/**
|
||||
* Computes the checksum synchronously on file. Convenience wrapper for computeNow().
|
||||
*/
|
||||
static QByteArray computeNowOnFile(const QString &filePath, CheckSums::Algorithm checksumType);
|
||||
|
||||
Q_SIGNALS:
|
||||
void done(CheckSums::Algorithm checksumType, const QByteArray &checksum);
|
||||
|
||||
private Q_SLOTS:
|
||||
void slotCalculationDone();
|
||||
|
||||
private:
|
||||
void startImpl(std::unique_ptr<QIODevice> device);
|
||||
|
||||
CheckSums::Algorithm _checksumType;
|
||||
|
||||
// watcher for the checksum calculation thread
|
||||
QFutureWatcher<QByteArray> _watcher;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether a file's checksum matches the expected value.
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT ValidateChecksumHeader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ValidateChecksumHeader(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* Check a file's actual checksum against the provided checksumHeader
|
||||
*
|
||||
* If no checksum is there, or if a correct checksum is there, the signal validated()
|
||||
* will be emitted. In case of any kind of error, the signal validationFailed() will
|
||||
* be emitted.
|
||||
*/
|
||||
void start(const QString &filePath, const QByteArray &checksumHeader);
|
||||
|
||||
/**
|
||||
* Check a device's actual checksum against the provided checksumHeader
|
||||
*
|
||||
* Like the other start() but works on an device.
|
||||
*
|
||||
* The device ownership transfers into the thread that
|
||||
* will compute the checksum. It must not have a parent.
|
||||
*/
|
||||
void start(std::unique_ptr<QIODevice> device, const QByteArray &checksumHeader);
|
||||
|
||||
Q_SIGNALS:
|
||||
void validated(CheckSums::Algorithm checksumType, const QByteArray &checksum);
|
||||
void validationFailed(const QString &errMsg);
|
||||
|
||||
private Q_SLOTS:
|
||||
void slotChecksumCalculated(CheckSums::Algorithm checksumType, const QByteArray &checksum);
|
||||
|
||||
private:
|
||||
ComputeChecksum *prepareStart(const QByteArray &checksumHeader);
|
||||
|
||||
ChecksumHeader _expectedChecksum;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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 "chronoelapsedtimer.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
using namespace OCC::Utility;
|
||||
using namespace std::chrono;
|
||||
|
||||
ChronoElapsedTimer::ChronoElapsedTimer(bool start)
|
||||
: _start(steady_clock::now())
|
||||
, _started(start)
|
||||
{
|
||||
}
|
||||
|
||||
bool ChronoElapsedTimer::isStarted() const
|
||||
{
|
||||
return _started;
|
||||
}
|
||||
|
||||
void ChronoElapsedTimer::reset()
|
||||
{
|
||||
_start = steady_clock::now();
|
||||
_end = {};
|
||||
_started = true;
|
||||
}
|
||||
|
||||
void ChronoElapsedTimer::stop()
|
||||
{
|
||||
Q_ASSERT(_end == steady_clock::time_point{});
|
||||
Q_ASSERT(_started);
|
||||
_end = steady_clock::now();
|
||||
// don't set _started to false, so that we can still call duration() later
|
||||
}
|
||||
|
||||
nanoseconds ChronoElapsedTimer::duration() const
|
||||
{
|
||||
if (!_started) {
|
||||
return nanoseconds::max();
|
||||
}
|
||||
if (_end != steady_clock::time_point{}) {
|
||||
return _end - _start;
|
||||
}
|
||||
return steady_clock::now() - _start;
|
||||
}
|
||||
|
||||
QDebug operator<<(QDebug debug, const ChronoElapsedTimer &timer)
|
||||
{
|
||||
QDebugStateSaver save(debug);
|
||||
debug.nospace();
|
||||
auto duration = timer.duration();
|
||||
const auto h = duration_cast<hours>(duration);
|
||||
const auto min = duration_cast<minutes>(duration -= h);
|
||||
const auto s = duration_cast<seconds>(duration -= min);
|
||||
const auto ms = duration_cast<milliseconds>(duration -= s);
|
||||
return debug << u"duration(" << h.count() << u"h, " << min.count() << u"min, " << s.count() << u"s, " << ms.count() << u"ms)";
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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
|
||||
*/
|
||||
#pragma once
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace OCC::Utility {
|
||||
|
||||
/**
|
||||
* Meassure time using std::chrono::steady_clock
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT ChronoElapsedTimer
|
||||
{
|
||||
public:
|
||||
ChronoElapsedTimer(bool start = true);
|
||||
|
||||
bool isStarted() const;
|
||||
/**
|
||||
* Resets and start the timer
|
||||
*/
|
||||
void reset();
|
||||
/**
|
||||
* Stops the timer
|
||||
*/
|
||||
void stop();
|
||||
/**
|
||||
* Returns the elapsed time.
|
||||
* If the timer is stopped it is the time between start and stop of the timer.
|
||||
*/
|
||||
std::chrono::nanoseconds duration() const;
|
||||
|
||||
private:
|
||||
std::chrono::steady_clock::time_point _start = {};
|
||||
std::chrono::steady_clock::time_point _end = {};
|
||||
bool _started = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
QSFERA_SYNC_EXPORT QDebug operator<<(QDebug debug, const OCC::Utility::ChronoElapsedTimer &in);
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <qglobal.h>
|
||||
|
||||
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102921
|
||||
// we hope it will be fixed in GCC >= 13
|
||||
#if !defined(Q_CC_GNU) || Q_CC_GNU >= 1300
|
||||
#define constexpr_list constexpr
|
||||
#else
|
||||
#define constexpr_list inline
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
#pragma once
|
||||
#include <qglobal.h>
|
||||
|
||||
#ifdef Q_CC_MSVC
|
||||
// _Pragma would require C99 ecm currently sets C90
|
||||
#define OC_DISABLE_DEPRECATED_WARNING \
|
||||
__pragma(warning(push)); \
|
||||
__pragma(warning(disable : 4996))
|
||||
|
||||
#define OC_ENABLE_DEPRECATED_WARNING __pragma(warning(pop))
|
||||
#else
|
||||
#define OC_DISABLE_DEPRECATED_WARNING \
|
||||
_Pragma("GCC diagnostic push"); \
|
||||
_Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")
|
||||
|
||||
#define OC_ENABLE_DEPRECATED_WARNING _Pragma("GCC diagnostic pop")
|
||||
#endif
|
||||
@@ -0,0 +1,587 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@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 "filesystembase.h"
|
||||
#include "common/asserts.h"
|
||||
#include "utility.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QSettings>
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "utility_win.h"
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
// Regarding
|
||||
// https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits
|
||||
// the file name limit appears to be about 255 chars
|
||||
// -10 for safety
|
||||
constexpr qsizetype fileNameMaxC = 255 - 10;
|
||||
|
||||
constexpr auto replacementCharC = QLatin1Char('_');
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcFileSystem, "sync.filesystem", QtInfoMsg)
|
||||
|
||||
std::filesystem::path FileSystem::toFilesystemPath(const QString &path)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
return QtPrivate::toFilesystemPath(FileSystem::longWinPath(path));
|
||||
#else
|
||||
return QtPrivate::toFilesystemPath(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
QString FileSystem::fromFilesystemPath(const std::filesystem::path &path)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
constexpr std::wstring_view prefix = LR"(\\?\)";
|
||||
std::wstring nativePath = path.native();
|
||||
auto view = std::wstring_view(nativePath);
|
||||
if (nativePath.starts_with(prefix)) {
|
||||
view = view.substr(prefix.size());
|
||||
}
|
||||
return QDir::fromNativeSeparators(QString::fromWCharArray(view.data(), view.length()));
|
||||
#elif defined(Q_OS_MACOS)
|
||||
// based on QFile::decodeName
|
||||
return QtPrivate::fromFilesystemPath(path).normalized(QString::NormalizationForm_C);
|
||||
#else
|
||||
return QtPrivate::fromFilesystemPath(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
QString FileSystem::longWinPath(const QString &inpath)
|
||||
{
|
||||
#ifndef Q_OS_WIN
|
||||
return inpath;
|
||||
#else
|
||||
Q_ASSERT(QFileInfo(inpath).isAbsolute());
|
||||
if (inpath.isEmpty()) {
|
||||
return inpath;
|
||||
}
|
||||
QString out = QDir::toNativeSeparators(inpath);
|
||||
const QLatin1Char sep('\\');
|
||||
if (out.size() == 2 && out.at(1) == ':'_L1) {
|
||||
// std::path handles C: incorrectly
|
||||
out += sep;
|
||||
}
|
||||
|
||||
// we already have a unc path
|
||||
if (out.startsWith(sep + sep)) {
|
||||
return out;
|
||||
}
|
||||
// prepend \\?\ and to support long names
|
||||
|
||||
if (out.at(0) == sep) {
|
||||
// should not happen as we require the path to be absolute
|
||||
return QStringLiteral("\\\\?") + out;
|
||||
}
|
||||
return QStringLiteral("\\\\?\\") + out;
|
||||
#endif
|
||||
}
|
||||
|
||||
void FileSystem::setFileHidden(const QString &filename, bool hidden)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
QString fName = longWinPath(filename);
|
||||
DWORD dwAttrs;
|
||||
|
||||
dwAttrs = GetFileAttributesW((wchar_t *)fName.utf16());
|
||||
|
||||
if (dwAttrs != INVALID_FILE_ATTRIBUTES) {
|
||||
if (hidden && !(dwAttrs & FILE_ATTRIBUTE_HIDDEN)) {
|
||||
SetFileAttributesW((wchar_t *)fName.utf16(), dwAttrs | FILE_ATTRIBUTE_HIDDEN);
|
||||
} else if (!hidden && (dwAttrs & FILE_ATTRIBUTE_HIDDEN)) {
|
||||
SetFileAttributesW((wchar_t *)fName.utf16(), dwAttrs & ~FILE_ATTRIBUTE_HIDDEN);
|
||||
}
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(filename);
|
||||
Q_UNUSED(hidden);
|
||||
#endif
|
||||
}
|
||||
|
||||
static QFile::Permissions getDefaultWritePermissions()
|
||||
{
|
||||
QFile::Permissions result = QFile::WriteUser;
|
||||
#ifndef Q_OS_WIN
|
||||
mode_t mask = umask(0);
|
||||
umask(mask);
|
||||
if (!(mask & S_IWGRP)) {
|
||||
result |= QFile::WriteGroup;
|
||||
}
|
||||
if (!(mask & S_IWOTH)) {
|
||||
result |= QFile::WriteOther;
|
||||
}
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
void FileSystem::setFileReadOnly(const QString &filename, bool readonly)
|
||||
{
|
||||
QFile file(filename);
|
||||
QFile::Permissions permissions = file.permissions();
|
||||
|
||||
QFile::Permissions allWritePermissions = QFile::WriteUser | QFile::WriteGroup | QFile::WriteOther | QFile::WriteOwner;
|
||||
static QFile::Permissions defaultWritePermissions = getDefaultWritePermissions();
|
||||
|
||||
permissions &= ~allWritePermissions;
|
||||
if (!readonly) {
|
||||
permissions |= defaultWritePermissions;
|
||||
}
|
||||
// Warning: This function does not manipulate ACLs, which may limit its effectiveness.
|
||||
file.setPermissions(permissions);
|
||||
}
|
||||
|
||||
void FileSystem::setFolderMinimumPermissions(const QString &filename)
|
||||
{
|
||||
#ifdef Q_OS_MAC
|
||||
QFile::Permissions perm = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
|
||||
QFile file(filename);
|
||||
file.setPermissions(perm);
|
||||
#else
|
||||
Q_UNUSED(filename);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void FileSystem::setFileReadOnlyWeak(const QString &filename, bool readonly)
|
||||
{
|
||||
QFile file(filename);
|
||||
QFile::Permissions permissions = file.permissions();
|
||||
|
||||
if (!readonly && (permissions & QFile::WriteOwner)) {
|
||||
return; // already writable enough
|
||||
}
|
||||
|
||||
setFileReadOnly(filename, readonly);
|
||||
}
|
||||
|
||||
bool FileSystem::rename(const QString &originFileName, const QString &destinationFileName, QString *errorString)
|
||||
{
|
||||
bool success = false;
|
||||
QString error;
|
||||
#ifdef Q_OS_WIN
|
||||
const QString originalFileNameLong = longWinPath(originFileName);
|
||||
const QString dest = longWinPath(destinationFileName);
|
||||
if (FileSystem::isFileLocked(dest, FileSystem::LockMode::Exclusive)) {
|
||||
error = QCoreApplication::translate("FileSystem", "Can't rename »%1«, the file is currently in use").arg(destinationFileName);
|
||||
} else if (FileSystem::isFileLocked(originalFileNameLong, FileSystem::LockMode::Exclusive)) {
|
||||
error = QCoreApplication::translate("FileSystem", "Can't rename »%1«, the file is currently in use").arg(originFileName);
|
||||
} else if (isLnkFile(originFileName) || isLnkFile(destinationFileName)) {
|
||||
success = MoveFileEx((wchar_t *)originalFileNameLong.utf16(), (wchar_t *)dest.utf16(), MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH);
|
||||
if (!success) {
|
||||
error = Utility::formatWinError(GetLastError());
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
QFile orig(originFileName);
|
||||
success = orig.rename(destinationFileName);
|
||||
if (!success) {
|
||||
error = orig.errorString();
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
qCWarning(lcFileSystem) << u"Error renaming file" << originFileName << u"to" << destinationFileName << u"failed: " << error;
|
||||
if (errorString) {
|
||||
*errorString = error;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool FileSystem::uncheckedRenameReplace(const QString &originFileName, const QString &destinationFileName, QString *errorString)
|
||||
{
|
||||
Q_ASSERT(errorString);
|
||||
#ifndef Q_OS_WIN
|
||||
bool success;
|
||||
QFile orig(originFileName);
|
||||
// We want a rename that also overwites. QFile::rename does not overwite.
|
||||
// Qt 5.1 has QSaveFile::renameOverwrite we could use.
|
||||
// ### FIXME
|
||||
success = true;
|
||||
bool destExists = fileExists(destinationFileName);
|
||||
if (destExists && !QFile::remove(destinationFileName)) {
|
||||
*errorString = orig.errorString();
|
||||
qCWarning(lcFileSystem) << u"Target file could not be removed.";
|
||||
success = false;
|
||||
}
|
||||
if (success) {
|
||||
success = orig.rename(destinationFileName);
|
||||
}
|
||||
if (!success) {
|
||||
*errorString = orig.errorString();
|
||||
qCWarning(lcFileSystem) << u"Renaming temp file to final failed: " << *errorString;
|
||||
return false;
|
||||
}
|
||||
|
||||
#else // Q_OS_WIN
|
||||
// You can not overwrite a read-only file on windows.
|
||||
|
||||
if (!QFileInfo(destinationFileName).isWritable()) {
|
||||
setFileReadOnly(destinationFileName, false);
|
||||
}
|
||||
const QString orig = longWinPath(originFileName);
|
||||
const QString dest = longWinPath(destinationFileName);
|
||||
if (FileSystem::isFileLocked(dest, FileSystem::LockMode::Exclusive)) {
|
||||
*errorString = QCoreApplication::translate("FileSystem", "Can't rename »%1«, the file is currently in use").arg(destinationFileName);
|
||||
qCWarning(lcFileSystem) << u"Renaming failed: " << *errorString;
|
||||
return false;
|
||||
}
|
||||
if (FileSystem::isFileLocked(orig, FileSystem::LockMode::Exclusive)) {
|
||||
*errorString = QCoreApplication::translate("FileSystem", "Can't rename »%1«, the file is currently in use").arg(originFileName);
|
||||
qCWarning(lcFileSystem) << u"Renaming failed: " << *errorString;
|
||||
return false;
|
||||
}
|
||||
const BOOL ok = MoveFileEx(reinterpret_cast<const wchar_t *>(orig.utf16()), reinterpret_cast<const wchar_t *>(dest.utf16()),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH);
|
||||
if (!ok) {
|
||||
*errorString = Utility::formatWinError(GetLastError());
|
||||
qCWarning(lcFileSystem) << u"Renaming temp file to final failed: " << *errorString;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FileSystem::openAndSeekFileSharedRead(QFile *file, QString *errorOrNull, qint64 seek)
|
||||
{
|
||||
QString errorDummy;
|
||||
// avoid many if (errorOrNull) later.
|
||||
QString &error = errorOrNull ? *errorOrNull : errorDummy;
|
||||
error.clear();
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
//
|
||||
// The following code is adapted from Qt's QFSFileEnginePrivate::nativeOpen()
|
||||
auto fileHandle = Utility::Handle::createHandle(toFilesystemPath(file->fileName()), {.accessMode = GENERIC_READ});
|
||||
|
||||
// Bail out on error.
|
||||
if (!fileHandle) {
|
||||
error = fileHandle.errorMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert the HANDLE to an fd and pass it to QFile's foreign-open
|
||||
// function. The fd owns the handle, so when QFile later closes
|
||||
// the fd the handle will be closed too.
|
||||
int fd = _open_osfhandle((intptr_t)fileHandle.handle(), _O_RDONLY);
|
||||
if (fd == -1) {
|
||||
error = QStringLiteral("could not make fd from handle");
|
||||
return false;
|
||||
}
|
||||
HANDLE basicHandle = fileHandle.release(); // we hand over the handle to qt
|
||||
if (!file->open(fd, QIODevice::ReadOnly, QFile::AutoCloseHandle)) {
|
||||
error = file->errorString();
|
||||
_close(fd); // implicitly closes fileHandle
|
||||
return false;
|
||||
}
|
||||
|
||||
// Seek to the right spot
|
||||
LARGE_INTEGER *li = reinterpret_cast<LARGE_INTEGER *>(&seek);
|
||||
DWORD newFilePointer = SetFilePointer(basicHandle, li->LowPart, &li->HighPart, FILE_BEGIN);
|
||||
const auto llastError = GetLastError();
|
||||
if (newFilePointer == 0xFFFFFFFF && llastError != NO_ERROR) {
|
||||
error = Utility::formatWinError(llastError);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
if (!file->open(QFile::ReadOnly)) {
|
||||
error = file->errorString();
|
||||
return false;
|
||||
}
|
||||
if (!file->seek(seek)) {
|
||||
error = file->errorString();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
static bool fileExistsWin(const QString &filename)
|
||||
{
|
||||
WIN32_FIND_DATA FindFileData;
|
||||
HANDLE hFind;
|
||||
QString fName = FileSystem::longWinPath(filename);
|
||||
|
||||
hFind = FindFirstFileW((wchar_t *)fName.utf16(), &FindFileData);
|
||||
if (hFind == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
FindClose(hFind);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool FileSystem::fileExists(const QString &filename, const QFileInfo &fileInfo)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
if (isLnkFile(filename)) {
|
||||
// Use a native check.
|
||||
return fileExistsWin(filename);
|
||||
}
|
||||
#endif
|
||||
bool re = fileInfo.exists();
|
||||
// if the filename is different from the filename in fileInfo, the fileInfo is
|
||||
// not valid. There needs to be one initialised here. Otherwise the incoming
|
||||
// fileInfo is re-used.
|
||||
if (fileInfo.filePath() != filename) {
|
||||
re = QFileInfo::exists(filename);
|
||||
}
|
||||
return re;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
QString FileSystem::fileSystemForPath(const QString &path)
|
||||
{
|
||||
// See also QStorageInfo (Qt >=5.4) and GetVolumeInformationByHandleW (>= Vista)
|
||||
QString drive = path.left(2);
|
||||
if (!drive.endsWith(QLatin1Char(':')))
|
||||
return QString();
|
||||
drive.append(QLatin1Char('\\'));
|
||||
|
||||
const size_t fileSystemBufferSize = 4096;
|
||||
TCHAR fileSystemBuffer[fileSystemBufferSize];
|
||||
|
||||
if (!GetVolumeInformationW(reinterpret_cast<LPCWSTR>(drive.utf16()), nullptr, 0, nullptr, nullptr, nullptr, fileSystemBuffer, fileSystemBufferSize)) {
|
||||
return QString();
|
||||
}
|
||||
return QString::fromUtf16(reinterpret_cast<const char16_t *>(fileSystemBuffer));
|
||||
}
|
||||
|
||||
bool FileSystem::longPathsEnabledOnWindows()
|
||||
{
|
||||
static std::optional<bool> longPathsEnabledCached = {};
|
||||
|
||||
if (!longPathsEnabledCached.has_value()) {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry#enable-long-paths-in-windows-10-version-1607-and-later
|
||||
QSettings fsSettings(QStringLiteral("HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\FileSystem"), QSettings::NativeFormat);
|
||||
QVariant longPathsEnabled = fsSettings.value(QStringLiteral("LongPathsEnabled"));
|
||||
qCDebug(lcFileSystem) << u"LongPathsEnabled:" << longPathsEnabled;
|
||||
longPathsEnabledCached = longPathsEnabled.value<uint32_t>() == 1;
|
||||
}
|
||||
|
||||
return longPathsEnabledCached.value();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool FileSystem::remove(const QString &fileName, QString *errorString)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
// You cannot delete a read-only file on windows, but we want to
|
||||
// allow that.
|
||||
|
||||
if (!QFileInfo(fileName).isWritable()) {
|
||||
setFileReadOnly(fileName, false);
|
||||
}
|
||||
#endif
|
||||
QFile f(fileName);
|
||||
if (!f.remove()) {
|
||||
if (errorString) {
|
||||
*errorString = f.errorString();
|
||||
}
|
||||
qCWarning(lcFileSystem) << u"Failed to delete:" << fileName << u"Error:" << f.errorString();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
namespace {
|
||||
/**
|
||||
* This function creates a file handle with the desired LockMode
|
||||
*/
|
||||
Utility::Handle lockFile(const QString &fileName, FileSystem::LockMode mode)
|
||||
{
|
||||
// Check if file exists
|
||||
const auto info = std::filesystem::directory_entry(FileSystem::toFilesystemPath(fileName));
|
||||
uint32_t shareMode = 0;
|
||||
uint32_t accessMode = GENERIC_READ | GENERIC_WRITE;
|
||||
switch (mode) {
|
||||
case FileSystem::LockMode::Exclusive:
|
||||
shareMode = 0;
|
||||
break;
|
||||
case FileSystem::LockMode::Shared:
|
||||
shareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
|
||||
break;
|
||||
case FileSystem::LockMode::SharedRead:
|
||||
shareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
|
||||
accessMode = GENERIC_READ;
|
||||
break;
|
||||
}
|
||||
if (info.exists()) {
|
||||
// Try to open the file with as much access as possible..
|
||||
auto out = Utility::Handle::createHandle(info.path(), {.accessMode = accessMode, .shareMode = shareMode});
|
||||
if (out) {
|
||||
if (info.is_directory()) {
|
||||
return out;
|
||||
} else {
|
||||
// try to lock the complete file explicitly, to check for ranged locks
|
||||
LARGE_INTEGER start;
|
||||
start.QuadPart = 0;
|
||||
LARGE_INTEGER end;
|
||||
end.QuadPart = -1;
|
||||
if (LockFile(out.handle(), start.LowPart, start.HighPart, end.LowPart, end.HighPart)) {
|
||||
OC_ENFORCE(UnlockFile(out.handle(), start.LowPart, start.HighPart, end.LowPart, end.HighPart));
|
||||
return out;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
bool FileSystem::isFileLocked(const QString &fileName, LockMode mode)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
const auto handle = lockFile(fileName, mode);
|
||||
if (!handle) {
|
||||
const auto error = GetLastError();
|
||||
if (error == ERROR_SHARING_VIOLATION || error == ERROR_LOCK_VIOLATION) {
|
||||
return true;
|
||||
} else if (error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) {
|
||||
const QFileInfo info(fileName);
|
||||
qCWarning(lcFileSystem) << Q_FUNC_INFO << mode << fileName << (info.isDir() ? "isDir" : (info.exists() ? "isFile" : "does not exist"))
|
||||
<< Utility::formatWinError(error);
|
||||
}
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(fileName);
|
||||
Q_UNUSED(mode);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FileSystem::isLnkFile(const QString &filename)
|
||||
{
|
||||
return filename.endsWith(QLatin1String(".lnk"));
|
||||
}
|
||||
|
||||
bool FileSystem::isJunction(const QString &filename)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
WIN32_FIND_DATA findData;
|
||||
HANDLE hFind =
|
||||
FindFirstFileEx(reinterpret_cast<const wchar_t *>(longWinPath(filename).utf16()), FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr, 0);
|
||||
if (hFind != INVALID_HANDLE_VALUE) {
|
||||
FindClose(hFind);
|
||||
return false;
|
||||
}
|
||||
return findData.dwFileAttributes != INVALID_FILE_ATTRIBUTES && findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT
|
||||
&& findData.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT;
|
||||
#else
|
||||
Q_UNUSED(filename);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
FileSystem::ChildResults FileSystem::isChildPathOf2(QStringView child, QStringView parent)
|
||||
{
|
||||
// if it is a relative path assume a local file, resolve it based on root
|
||||
const auto sensitivity = Utility::fsCaseSensitivity();
|
||||
|
||||
// Fast-path the 3 common cases in this if-else statement:
|
||||
if (parent.isEmpty()) {
|
||||
// The empty parent is often used as the sync root, as (child) items do not start with a `/`
|
||||
return ChildResult::IsChild | ChildResult::IsParentEmpty;
|
||||
}
|
||||
if (child.compare(parent, sensitivity) == 0) {
|
||||
return ChildResult::IsChild | ChildResult::IsEqual;
|
||||
}
|
||||
const auto isSeparator = [](QChar c) { return c == QLatin1Char('/') || (Utility::isWindows() && c == QLatin1Char('\\')); };
|
||||
if (isSeparator(parent.back())) {
|
||||
// Here we can do a normal prefix check, because the parent is "terminated" with a slash,
|
||||
// and we can't walk into the case in the else below.
|
||||
if (child.startsWith(parent, sensitivity)) {
|
||||
return ChildResult::IsChild;
|
||||
}
|
||||
// else: do the `cleanPath` version below
|
||||
}
|
||||
|
||||
// Slow path (`QDir::cleanPath` does lots of string operations):
|
||||
const QString cleanParent = QDir::cleanPath(parent.toString());
|
||||
const QString cleanChild = QDir::cleanPath(child.toString());
|
||||
// both paths are the same /root/foo == /root/foo
|
||||
if (cleanChild.compare(cleanParent, sensitivity) == 0) {
|
||||
return ChildResult::IsChild | ChildResult::IsEqual;
|
||||
}
|
||||
// cleanPath removes trailing slashes, add one to parent to be sure we handle a child path
|
||||
// /root/foo/bar is not a child of /root/fo, therefore, the trailing slash is important
|
||||
if (cleanChild.startsWith(cleanParent + QLatin1Char('/'), sensitivity)) {
|
||||
return ChildResult::IsChild;
|
||||
}
|
||||
return ChildResult::IsNoChild;
|
||||
}
|
||||
|
||||
QString FileSystem::createPortableFileName(const QString &path, const QString &fileName, qsizetype reservedSize)
|
||||
{
|
||||
// remove leading and trailing whitespace
|
||||
QString tmp = pathEscape(fileName);
|
||||
// limit size to fileNameMaxC
|
||||
tmp.resize(std::min<qsizetype>(tmp.size(), fileNameMaxC - reservedSize));
|
||||
// remove eventual trailing whitespace after the resize
|
||||
tmp = tmp.trimmed();
|
||||
return QDir::cleanPath(path + QLatin1Char('/') + tmp);
|
||||
}
|
||||
|
||||
QString FileSystem::pathEscape(const QString &s)
|
||||
{
|
||||
QString tmp = s;
|
||||
tmp.replace(QLatin1String("../"), QString(replacementCharC));
|
||||
#ifdef Q_OS_WIN
|
||||
tmp.replace(QLatin1String("..\\"), QString(replacementCharC));
|
||||
#endif
|
||||
|
||||
// escape the folder separator, "\" is handled in IllegalFilenameCharsWindows
|
||||
tmp.replace(QLatin1Char('/'), replacementCharC);
|
||||
|
||||
// replace non portable characters
|
||||
for (auto c : IllegalFilenameCharsWindows) {
|
||||
tmp.replace(c, replacementCharC);
|
||||
}
|
||||
return tmp.trimmed();
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#include "moc_filesystembase.cpp"
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@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
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "constexpr_list.h"
|
||||
#include "libsync/qsferasynclib.h"
|
||||
#include "utility.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QLoggingCategory>
|
||||
#include <QString>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
|
||||
class QFile;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
QSFERA_SYNC_EXPORT Q_DECLARE_LOGGING_CATEGORY(lcFileSystem)
|
||||
|
||||
/**
|
||||
* \addtogroup libsync
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief This file contains file system helper
|
||||
*/
|
||||
namespace FileSystem
|
||||
{
|
||||
QSFERA_SYNC_EXPORT Q_NAMESPACE;
|
||||
|
||||
QSFERA_SYNC_EXPORT std::filesystem::path toFilesystemPath(const QString &path);
|
||||
QSFERA_SYNC_EXPORT QString fromFilesystemPath(const std::filesystem::path &path);
|
||||
|
||||
|
||||
/**
|
||||
* List of characters not allowd in filenames on Windows
|
||||
*/
|
||||
constexpr_list auto IllegalFilenameCharsWindows = {
|
||||
QLatin1Char('\\'), QLatin1Char(':'), QLatin1Char('?'), QLatin1Char('*'), QLatin1Char('"'), QLatin1Char('>'), QLatin1Char('<'), QLatin1Char('|')};
|
||||
|
||||
/**
|
||||
* @brief Mark the file as hidden (only has effects on windows)
|
||||
*/
|
||||
void QSFERA_SYNC_EXPORT setFileHidden(const QString &filename, bool hidden);
|
||||
|
||||
/**
|
||||
* @brief Marks the file as read-only.
|
||||
*
|
||||
* On linux this either revokes all 'w' permissions or restores permissions
|
||||
* according to the umask.
|
||||
*/
|
||||
void QSFERA_SYNC_EXPORT setFileReadOnly(const QString &filename, bool readonly);
|
||||
|
||||
/**
|
||||
* @brief Marks the file as read-only.
|
||||
*
|
||||
* It's like setFileReadOnly(), but weaker: if readonly is false and the user
|
||||
* already has write permissions, no change to the permissions is made.
|
||||
*
|
||||
* This means that it will preserve explicitly set rw-r--r-- permissions even
|
||||
* when the umask is 0002. (setFileReadOnly() would adjust to rw-rw-r--)
|
||||
*/
|
||||
void QSFERA_SYNC_EXPORT setFileReadOnlyWeak(const QString &filename, bool readonly);
|
||||
|
||||
/**
|
||||
* @brief Try to set permissions so that other users on the local machine can not
|
||||
* go into the folder.
|
||||
*/
|
||||
void QSFERA_SYNC_EXPORT setFolderMinimumPermissions(const QString &filename);
|
||||
|
||||
/*
|
||||
* This function takes a path and converts it to a UNC representation of the
|
||||
* string. That means that it prepends a \\?\ (unless already UNC) and converts
|
||||
* all slashes to backslashes.
|
||||
*
|
||||
* Note the following:
|
||||
* - The string must be absolute.
|
||||
* - it needs to contain a drive character to be a valid UNC
|
||||
* - A conversion is only done if the path len is larger than 245. Otherwise
|
||||
* the windows API functions work with the normal "unixoid" representation too.
|
||||
*/
|
||||
QString QSFERA_SYNC_EXPORT longWinPath(const QString &inpath);
|
||||
|
||||
/**
|
||||
* @brief Checks whether a file exists.
|
||||
*
|
||||
* Use this over QFileInfo::exists() and QFile::exists() to avoid bugs with lnk
|
||||
* files, see above.
|
||||
*/
|
||||
bool QSFERA_SYNC_EXPORT fileExists(const QString &filename, const QFileInfo & = QFileInfo());
|
||||
|
||||
/**
|
||||
* @brief Rename the file \a originFileName to \a destinationFileName.
|
||||
*
|
||||
* It behaves as QFile::rename() but handles .lnk files correctly on Windows.
|
||||
*/
|
||||
bool QSFERA_SYNC_EXPORT rename(const QString &originFileName, const QString &destinationFileName, QString *errorString = nullptr);
|
||||
|
||||
/**
|
||||
* Rename the file \a originFileName to \a destinationFileName, and
|
||||
* overwrite the destination if it already exists - without extra checks.
|
||||
*/
|
||||
bool QSFERA_SYNC_EXPORT uncheckedRenameReplace(const QString &originFileName, const QString &destinationFileName, QString *errorString);
|
||||
|
||||
/**
|
||||
* Removes a file.
|
||||
*
|
||||
* Equivalent to QFile::remove(), except on Windows, where it will also
|
||||
* successfully remove read-only files.
|
||||
*/
|
||||
bool QSFERA_SYNC_EXPORT remove(const QString &fileName, QString *errorString = nullptr);
|
||||
|
||||
/**
|
||||
* Replacement for QFile::open(ReadOnly) followed by a seek().
|
||||
* This version sets a more permissive sharing mode on Windows.
|
||||
*
|
||||
* Warning: The resulting file may have an empty fileName and be unsuitable for use
|
||||
* with QFileInfo! Calling seek() on the QFile with >32bit signed values will fail!
|
||||
*/
|
||||
bool QSFERA_SYNC_EXPORT openAndSeekFileSharedRead(QFile * file, QString * error, qint64 seek);
|
||||
|
||||
enum class LockMode { Shared, Exclusive, SharedRead };
|
||||
Q_ENUM_NS(LockMode);
|
||||
/**
|
||||
* Returns true when a file is locked. (Windows only)
|
||||
*/
|
||||
bool QSFERA_SYNC_EXPORT isFileLocked(const QString &fileName, LockMode mode);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
|
||||
bool QSFERA_SYNC_EXPORT longPathsEnabledOnWindows();
|
||||
|
||||
/**
|
||||
* Returns the file system used at the given path.
|
||||
*/
|
||||
QString QSFERA_SYNC_EXPORT fileSystemForPath(const QString &path);
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Returns whether the file is a shortcut file (ends with .lnk)
|
||||
*/
|
||||
bool QSFERA_SYNC_EXPORT isLnkFile(const QString &filename);
|
||||
|
||||
/**
|
||||
* Returns whether the file is a junction (windows only)
|
||||
*/
|
||||
bool QSFERA_SYNC_EXPORT isJunction(const QString &filename);
|
||||
|
||||
/**
|
||||
* Returns whether a Path is a child of another
|
||||
*/
|
||||
enum class ChildResult : uint8_t { IsNoChild = 0, IsChild = 0x1 << 0, IsEqual = 0x1 << 1, IsParentEmpty = 0x1 << 2 };
|
||||
Q_FLAG_NS(ChildResult);
|
||||
Q_DECLARE_FLAGS(ChildResults, ChildResult)
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(ChildResults)
|
||||
|
||||
ChildResults QSFERA_SYNC_EXPORT isChildPathOf2(QStringView child, QStringView parent);
|
||||
inline bool isChildPathOf(QStringView child, QStringView parent)
|
||||
{
|
||||
return isChildPathOf2(child, parent) & ChildResult::IsChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the file name length is allowed on all platforms and the file name does not contain illegal characters
|
||||
* reservedSize: The resulting path will be reservedSize < MAX and allows appending.
|
||||
*/
|
||||
QString QSFERA_SYNC_EXPORT createPortableFileName(const QString &path, const QString &fileName, qsizetype reservedSize = 0);
|
||||
|
||||
/*
|
||||
* Replace path navigation elements from the string
|
||||
*/
|
||||
QString QSFERA_SYNC_EXPORT pathEscape(const QString &s);
|
||||
|
||||
namespace SizeLiterals {
|
||||
constexpr auto BinaryBase = 1024;
|
||||
constexpr auto DecimalBase = 1000;
|
||||
|
||||
constexpr unsigned long long operator"" _B(unsigned long long sz)
|
||||
{
|
||||
return sz;
|
||||
}
|
||||
|
||||
constexpr unsigned long long operator"" _KiB(unsigned long long sz)
|
||||
{
|
||||
return operator"" _B(sz) * BinaryBase;
|
||||
}
|
||||
|
||||
constexpr unsigned long long operator"" _MiB(unsigned long long sz)
|
||||
{
|
||||
return operator"" _KiB(sz) * BinaryBase;
|
||||
}
|
||||
|
||||
constexpr unsigned long long operator"" _GiB(unsigned long long sz)
|
||||
{
|
||||
return operator"" _MiB(sz) * BinaryBase;
|
||||
}
|
||||
|
||||
constexpr unsigned long long operator"" _kB(unsigned long long sz)
|
||||
{
|
||||
return operator"" _B(sz) * DecimalBase;
|
||||
}
|
||||
|
||||
constexpr unsigned long long operator"" _MB(unsigned long long sz)
|
||||
{
|
||||
return operator"" _kB(sz) * DecimalBase;
|
||||
}
|
||||
|
||||
constexpr unsigned long long operator"" _GB(unsigned long long sz)
|
||||
{
|
||||
return operator"" _MB(sz) * DecimalBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @} */
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* A Fixed sized ring buffer optimized on continouous insertion
|
||||
*/
|
||||
template <typename TYPE>
|
||||
class FixedSizeRingBuffer
|
||||
{
|
||||
public:
|
||||
FixedSizeRingBuffer(size_t size)
|
||||
: _data(size, TYPE())
|
||||
{
|
||||
}
|
||||
|
||||
constexpr size_t capacity() const { return _data.size(); }
|
||||
|
||||
constexpr bool isFull() const { return size() >= _data.size(); }
|
||||
|
||||
constexpr size_t size() const { return _end - _start; }
|
||||
|
||||
constexpr bool empty() const { return size() == 0; }
|
||||
|
||||
// iterators pointing to the raw data, the range might be smaller than _Size
|
||||
// if ever needed we could implement a iterator for the data window
|
||||
auto begin() { return _data.begin(); }
|
||||
|
||||
auto end() { return isFull() ? _data.end() : _data.begin() + size(); }
|
||||
|
||||
auto cbegin() const { return _data.cbegin(); }
|
||||
|
||||
auto cend() const { return isFull() ? _data.cend() : _data.cbegin() + size(); }
|
||||
|
||||
void pop_front()
|
||||
{
|
||||
// move the windows without changing the underlying data
|
||||
_start++;
|
||||
Q_ASSERT(_start < _end);
|
||||
// adjust offset to prevent overflow
|
||||
const auto s = size();
|
||||
_start %= _data.size();
|
||||
_end = _start + s;
|
||||
}
|
||||
|
||||
void push_back(TYPE &&data)
|
||||
{
|
||||
Q_ASSERT(!isFull());
|
||||
_data[convertToIndex(size())] = data;
|
||||
_end++;
|
||||
}
|
||||
|
||||
const TYPE &at(size_t index) const { return _data.at(convertToIndex(index)); }
|
||||
|
||||
/*
|
||||
* Remove items if f returns true
|
||||
* The filtered result is unordered
|
||||
*/
|
||||
void remove_if(const std::function<bool(const TYPE &)> &f)
|
||||
{
|
||||
// filter and sort the data
|
||||
FixedSizeRingBuffer<TYPE> tmp(_data.size());
|
||||
const auto start = convertToIndex(0);
|
||||
for (auto it = begin() + start; it != end(); ++it) {
|
||||
if (!f(*it)) {
|
||||
tmp.push_back(std::move(*it));
|
||||
}
|
||||
}
|
||||
for (auto it = begin(); it != begin() + start; ++it) {
|
||||
if (!f(*it)) {
|
||||
tmp.push_back(std::move(*it));
|
||||
}
|
||||
}
|
||||
*this = std::move(tmp);
|
||||
}
|
||||
|
||||
void reset(std::vector<TYPE> &&data)
|
||||
{
|
||||
Q_ASSERT(data.size() <= _data.size());
|
||||
_start = 0;
|
||||
_end = data.size();
|
||||
std::move(data.begin(), data.end(), _data.begin());
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<TYPE> _data;
|
||||
|
||||
// the sliding window of the ring buffer
|
||||
size_t _start = 0;
|
||||
size_t _end = 0;
|
||||
|
||||
// converts an array index to the underlying array index
|
||||
constexpr size_t convertToIndex(size_t i) const { return (_start + i) % _data.size(); }
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
/*
|
||||
* 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 <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QString>
|
||||
|
||||
#include "common/asserts.h"
|
||||
#include "common/filesystembase.h"
|
||||
#include "common/utility.h"
|
||||
|
||||
#include "ownsql.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
constexpr int SQLITE_REPEAT_COUNT = 20;
|
||||
|
||||
}
|
||||
|
||||
#define SQLITE_DO(A) \
|
||||
if (1) { \
|
||||
_errId = (A); \
|
||||
if (_errId != SQLITE_OK && _errId != SQLITE_DONE && _errId != SQLITE_ROW) { \
|
||||
_error = QString::fromUtf8(sqlite3_errmsg(_db)); \
|
||||
} \
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcSql, "sync.database.sql", QtInfoMsg)
|
||||
|
||||
SqlDatabase::SqlDatabase()
|
||||
: _db(nullptr)
|
||||
, _errId(0)
|
||||
{
|
||||
}
|
||||
|
||||
SqlDatabase::~SqlDatabase()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
|
||||
bool SqlDatabase::isOpen() const
|
||||
{
|
||||
return _db != nullptr;
|
||||
}
|
||||
|
||||
bool SqlDatabase::openHelper(const QString &filename, int sqliteFlags)
|
||||
{
|
||||
if (isOpen()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
sqliteFlags |= SQLITE_OPEN_NOMUTEX;
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// allow file paths > 260
|
||||
// https://www.sqlite.org/vfs.html#standard_windows_vfses
|
||||
const char *vfs = "win32-longpath";
|
||||
#else
|
||||
char *vfs = nullptr;
|
||||
#endif
|
||||
SQLITE_DO(sqlite3_open_v2(FileSystem::longWinPath(filename).toUtf8().constData(), &_db, sqliteFlags, vfs));
|
||||
|
||||
if (_errId != SQLITE_OK) {
|
||||
qCWarning(lcSql) << u"Error:" << _error << u"for" << filename;
|
||||
if (_errId == SQLITE_CANTOPEN) {
|
||||
qCWarning(lcSql) << u"CANTOPEN extended errcode: " << sqlite3_extended_errcode(_db);
|
||||
#if SQLITE_VERSION_NUMBER >= 3012000
|
||||
qCWarning(lcSql) << u"CANTOPEN system errno: " << sqlite3_system_errno(_db);
|
||||
#endif
|
||||
}
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_db) {
|
||||
qCWarning(lcSql) << u"Error: no database for" << filename;
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_busy_timeout(_db, 5000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SqlDatabase::CheckDbResult SqlDatabase::checkDb()
|
||||
{
|
||||
// quick_check can fail with a disk IO error when diskspace is low
|
||||
SqlQuery quick_check(*this);
|
||||
|
||||
if (quick_check.prepare("PRAGMA quick_check;", /*allow_failure=*/true) != SQLITE_OK) {
|
||||
qCWarning(lcSql) << u"Error preparing quick_check on database";
|
||||
_errId = quick_check.errorId();
|
||||
_error = quick_check.error();
|
||||
return CheckDbResult::CantPrepare;
|
||||
}
|
||||
if (!quick_check.exec()) {
|
||||
qCWarning(lcSql) << u"Error running quick_check on database";
|
||||
_errId = quick_check.errorId();
|
||||
_error = quick_check.error();
|
||||
return CheckDbResult::CantExec;
|
||||
}
|
||||
|
||||
quick_check.next();
|
||||
QString result = quick_check.stringValue(0);
|
||||
if (result != QLatin1String("ok")) {
|
||||
qCWarning(lcSql) << u"quick_check returned failure:" << result;
|
||||
return CheckDbResult::NotOk;
|
||||
}
|
||||
|
||||
return CheckDbResult::Ok;
|
||||
}
|
||||
|
||||
bool SqlDatabase::openOrCreateReadWrite(const QString &filename)
|
||||
{
|
||||
if (isOpen()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!openHelper(filename, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto checkResult = checkDb();
|
||||
if (checkResult != CheckDbResult::Ok) {
|
||||
close();
|
||||
if (checkResult == CheckDbResult::CantPrepare) {
|
||||
// When disk space is low, preparing may fail even though the db is fine.
|
||||
// Typically CANTOPEN or IOERR.
|
||||
qint64 freeSpace = Utility::freeDiskSpace(QFileInfo(filename).dir().absolutePath());
|
||||
if (freeSpace != -1 && freeSpace < 1000000) {
|
||||
qCWarning(lcSql) << u"Can't prepare consistency check and disk space is low:" << freeSpace;
|
||||
} else if (_errId == SQLITE_CANTOPEN) {
|
||||
// Even when there's enough disk space, it might very well be that the
|
||||
// file is on a read-only filesystem and can't be opened because of that.
|
||||
qCWarning(lcSql) << u"Can't open db to prepare consistency check, aborting";
|
||||
} else if (_errId == SQLITE_LOCKED || _errId == SQLITE_BUSY) {
|
||||
qCWarning(lcSql) << u"Can't open db to prepare consistency check, the db is locked aborting" << _errId << _error;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
qCCritical(lcSql) << u"Consistency check failed, removing broken db" << filename;
|
||||
QFile fileToRemove(filename);
|
||||
if (!fileToRemove.remove()) {
|
||||
qCCritical(lcSql) << u"Failed to remove broken db" << filename << u":" << fileToRemove.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
return openHelper(filename, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SqlDatabase::openReadOnly(const QString &filename)
|
||||
{
|
||||
if (isOpen()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!openHelper(filename, SQLITE_OPEN_READONLY)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkDb() != CheckDbResult::Ok) {
|
||||
qCWarning(lcSql) << u"Consistency check failed in readonly mode, giving up" << filename;
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString SqlDatabase::error() const
|
||||
{
|
||||
const QString err(_error);
|
||||
// _error.clear();
|
||||
return err;
|
||||
}
|
||||
|
||||
void SqlDatabase::close()
|
||||
{
|
||||
if (_db) {
|
||||
// we need a copy because query is removed from a set in finish()
|
||||
const auto queries = _queries;
|
||||
for (auto *q : queries) {
|
||||
q->finish();
|
||||
}
|
||||
SQLITE_DO(sqlite3_close(_db));
|
||||
if (_errId != SQLITE_OK)
|
||||
qCWarning(lcSql) << u"Closing database failed" << _error;
|
||||
_db = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool SqlDatabase::transaction()
|
||||
{
|
||||
if (!_db) {
|
||||
return false;
|
||||
}
|
||||
SQLITE_DO(sqlite3_exec(_db, "BEGIN", nullptr, nullptr, nullptr));
|
||||
return _errId == SQLITE_OK;
|
||||
}
|
||||
|
||||
bool SqlDatabase::commit()
|
||||
{
|
||||
if (!_db) {
|
||||
return false;
|
||||
}
|
||||
SQLITE_DO(sqlite3_exec(_db, "COMMIT", nullptr, nullptr, nullptr));
|
||||
return _errId == SQLITE_OK;
|
||||
}
|
||||
|
||||
sqlite3 *SqlDatabase::sqliteDb()
|
||||
{
|
||||
return _db;
|
||||
}
|
||||
|
||||
/* =========================================================================================== */
|
||||
|
||||
SqlQuery::SqlQuery(SqlDatabase &db)
|
||||
: _sqldb(&db)
|
||||
, _db(db.sqliteDb())
|
||||
{
|
||||
}
|
||||
|
||||
SqlQuery::~SqlQuery()
|
||||
{
|
||||
if (_stmt) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
SqlQuery::SqlQuery(const QByteArray &sql, SqlDatabase &db)
|
||||
: _sqldb(&db)
|
||||
, _db(db.sqliteDb())
|
||||
{
|
||||
prepare(sql);
|
||||
}
|
||||
|
||||
int SqlQuery::prepare(const QByteArray &sql, bool allow_failure)
|
||||
{
|
||||
_sql = sql.trimmed();
|
||||
if (_stmt) {
|
||||
finish();
|
||||
}
|
||||
if (!_sql.isEmpty()) {
|
||||
for (int n = 0; n < SQLITE_REPEAT_COUNT; ++n) {
|
||||
qCDebug(lcSql) << u"SQL prepare" << _sql << u"Try:" << n;
|
||||
_errId = sqlite3_prepare_v2(_db, _sql.constData(), -1, &_stmt, nullptr);
|
||||
if (_errId != SQLITE_OK) {
|
||||
qCWarning(lcSql) << u"SQL prepare failed" << _sql << QString::fromUtf8(sqlite3_errmsg(_db));
|
||||
if ((_errId == SQLITE_BUSY) || (_errId == SQLITE_LOCKED)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (_errId != SQLITE_OK) {
|
||||
_error = QString::fromUtf8(sqlite3_errmsg(_db));
|
||||
qCWarning(lcSql) << u"Sqlite prepare statement error:" << _errId << _error << u"in" << _sql;
|
||||
OC_ENFORCE_X(allow_failure, "SQLITE Prepare error");
|
||||
} else {
|
||||
OC_ASSERT(_stmt);
|
||||
_sqldb->_queries.insert(this);
|
||||
|
||||
if (lcSql().isDebugEnabled()) {
|
||||
_boundValues.resize(sqlite3_bind_parameter_count(_stmt));
|
||||
for (int i = 1; i <= _boundValues.size(); ++i) {
|
||||
const auto name = QRegularExpression::escape(QString::fromUtf8(sqlite3_bind_parameter_name(_stmt, i)));
|
||||
_boundValues[i - 1].name = QRegularExpression(uR"(%1\b)"_s.arg(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return _errId;
|
||||
}
|
||||
|
||||
/**
|
||||
* There is no overloads to QByteArray::startWith that takes Qt::CaseInsensitive.
|
||||
* Returns true if 'a' starts with 'b' in a case insensitive way
|
||||
*/
|
||||
static bool startsWithInsensitive(const QByteArray &a, const QByteArray &b)
|
||||
{
|
||||
return a.size() >= b.size() && qstrnicmp(a.constData(), b.constData(), b.size()) == 0;
|
||||
}
|
||||
|
||||
bool SqlQuery::isSelect()
|
||||
{
|
||||
return startsWithInsensitive(_sql, QByteArrayLiteral("SELECT"));
|
||||
}
|
||||
|
||||
bool SqlQuery::isPragma()
|
||||
{
|
||||
return startsWithInsensitive(_sql, QByteArrayLiteral("PRAGMA"));
|
||||
}
|
||||
|
||||
bool SqlQuery::exec()
|
||||
{
|
||||
if (!_stmt) {
|
||||
qCWarning(lcSql) << u"Can't exec query, statement unprepared.";
|
||||
return false;
|
||||
}
|
||||
// Don't do anything for selects, that is how we use the lib :-|
|
||||
if (!isSelect() && !isPragma()) {
|
||||
for (int n = 0; n < SQLITE_REPEAT_COUNT; ++n) {
|
||||
if (lcSql().isDebugEnabled()) {
|
||||
if (!_boundValues.isEmpty()) {
|
||||
// Try to create a pretty printed version of the sql query
|
||||
// the result can be inaccurate
|
||||
QString query = QString::fromUtf8(_sql);
|
||||
for (const auto &v : _boundValues) {
|
||||
query.replace(v.name, v.value);
|
||||
}
|
||||
char *actualQuery = sqlite3_expanded_sql(_stmt);
|
||||
qCDebug(lcSql) << u"SQL exec: Estimated query:" << query << u"Actual query:" << QString::fromUtf8(actualQuery) << u"Try:" << n;
|
||||
sqlite3_free(actualQuery);
|
||||
} else {
|
||||
qCDebug(lcSql) << u"SQL exec:" << _sql << u"Try:" << n;
|
||||
}
|
||||
}
|
||||
_errId = sqlite3_step(_stmt);
|
||||
if (_errId != SQLITE_DONE && _errId != SQLITE_ROW) {
|
||||
qCWarning(lcSql) << u"SQL exec failed" << _sql << QString::fromUtf8(sqlite3_errmsg(_db));
|
||||
if (_errId == SQLITE_LOCKED || _errId == SQLITE_BUSY) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (_errId != SQLITE_DONE && _errId != SQLITE_ROW) {
|
||||
_error = QString::fromUtf8(sqlite3_errmsg(_db));
|
||||
qCWarning(lcSql) << u"Sqlite exec statement error:" << _errId << _error << u"in" << _sql;
|
||||
if (_errId == SQLITE_IOERR) {
|
||||
qCWarning(lcSql) << u"IOERR extended errcode: " << sqlite3_extended_errcode(_db);
|
||||
#if SQLITE_VERSION_NUMBER >= 3012000
|
||||
qCWarning(lcSql) << u"IOERR system errno: " << sqlite3_system_errno(_db);
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
qCDebug(lcSql) << u"Last exec affected" << numRowsAffected() << u"rows.";
|
||||
}
|
||||
return (_errId == SQLITE_DONE); // either SQLITE_ROW or SQLITE_DONE
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto SqlQuery::next() -> NextResult
|
||||
{
|
||||
const bool firstStep = !sqlite3_stmt_busy(_stmt);
|
||||
|
||||
for (int n = 0; n < SQLITE_REPEAT_COUNT; ++n) {
|
||||
_errId = sqlite3_step(_stmt);
|
||||
if (!(firstStep && (_errId == SQLITE_LOCKED || _errId == SQLITE_BUSY))) {
|
||||
// For the first step: retry if the database is locked or busy.
|
||||
// For any other iteration: the first one succeeded, so subsequent steps should also
|
||||
// succeed. If not, bail out and report an error (done below).
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
NextResult result;
|
||||
result.ok = _errId == SQLITE_ROW || _errId == SQLITE_DONE;
|
||||
result.hasData = _errId == SQLITE_ROW;
|
||||
if (!result.ok) {
|
||||
_error = QString::fromUtf8(sqlite3_errmsg(_db));
|
||||
qCWarning(lcSql) << u"Sqlite step statement error:" << _errId << _error << u"in" << _sql;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SqlQuery::bindValueInternal(int pos, const QVariant &value)
|
||||
{
|
||||
int res = -1;
|
||||
if (!_stmt) {
|
||||
OC_ASSERT(false);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (value.typeId()) {
|
||||
case QMetaType::Int:
|
||||
case QMetaType::Bool:
|
||||
res = sqlite3_bind_int(_stmt, pos, value.toInt());
|
||||
break;
|
||||
case QMetaType::Double:
|
||||
res = sqlite3_bind_double(_stmt, pos, value.toDouble());
|
||||
break;
|
||||
case QMetaType::UInt:
|
||||
case QMetaType::Long:
|
||||
case QMetaType::ULong:
|
||||
case QMetaType::LongLong:
|
||||
case QMetaType::ULongLong:
|
||||
res = sqlite3_bind_int64(_stmt, pos, value.toLongLong());
|
||||
break;
|
||||
case QMetaType::QString: {
|
||||
if (!value.isNull()) {
|
||||
// lifetime of string == lifetime of its qvariant
|
||||
const QString *str = static_cast<const QString *>(value.constData());
|
||||
res = sqlite3_bind_text16(_stmt, pos, str->utf16(), static_cast<int>(str->size() * sizeof(ushort)), SQLITE_TRANSIENT);
|
||||
} else {
|
||||
res = sqlite3_bind_null(_stmt, pos);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QMetaType::QByteArray: {
|
||||
auto ba = value.toByteArray();
|
||||
res = sqlite3_bind_text(_stmt, pos, ba.constData(), ba.size(), SQLITE_TRANSIENT);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
qCWarning(lcSql) << u"Unsupported raw type detected" << value.typeId() << value.typeName() << value.metaType().sizeOf() << value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (res != SQLITE_OK) {
|
||||
qCWarning(lcSql) << u"ERROR binding SQL value:" << value << u"error:" << res;
|
||||
}
|
||||
OC_ASSERT(res == SQLITE_OK);
|
||||
}
|
||||
|
||||
bool SqlQuery::nullValue(int index)
|
||||
{
|
||||
return sqlite3_column_type(_stmt, index) == SQLITE_NULL;
|
||||
}
|
||||
|
||||
QString SqlQuery::stringValue(int index)
|
||||
{
|
||||
return QString::fromUtf16(static_cast<const char16_t *>(sqlite3_column_text16(_stmt, index)));
|
||||
}
|
||||
|
||||
int SqlQuery::intValue(int index)
|
||||
{
|
||||
return sqlite3_column_int(_stmt, index);
|
||||
}
|
||||
|
||||
quint64 SqlQuery::int64Value(int index)
|
||||
{
|
||||
return sqlite3_column_int64(_stmt, index);
|
||||
}
|
||||
|
||||
QByteArray SqlQuery::baValue(int index)
|
||||
{
|
||||
return QByteArray(static_cast<const char *>(sqlite3_column_blob(_stmt, index)), sqlite3_column_bytes(_stmt, index));
|
||||
}
|
||||
|
||||
QString SqlQuery::error() const
|
||||
{
|
||||
return _error;
|
||||
}
|
||||
|
||||
int SqlQuery::errorId() const
|
||||
{
|
||||
return _errId;
|
||||
}
|
||||
|
||||
const QByteArray &SqlQuery::lastQuery() const
|
||||
{
|
||||
return _sql;
|
||||
}
|
||||
|
||||
int SqlQuery::numRowsAffected()
|
||||
{
|
||||
return sqlite3_changes(_db);
|
||||
}
|
||||
|
||||
void SqlQuery::finish()
|
||||
{
|
||||
if (!_stmt)
|
||||
return;
|
||||
SQLITE_DO(sqlite3_finalize(_stmt));
|
||||
_stmt = nullptr;
|
||||
if (_sqldb) {
|
||||
_sqldb->_queries.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
void SqlQuery::reset_and_clear_bindings()
|
||||
{
|
||||
if (_stmt) {
|
||||
SQLITE_DO(sqlite3_reset(_stmt));
|
||||
SQLITE_DO(sqlite3_clear_bindings(_stmt));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef OWNSQL_H
|
||||
#define OWNSQL_H
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QVariant>
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
struct sqlite3;
|
||||
struct sqlite3_stmt;
|
||||
|
||||
namespace OCC {
|
||||
QSFERA_SYNC_EXPORT Q_DECLARE_LOGGING_CATEGORY(lcSql)
|
||||
|
||||
class SqlQuery;
|
||||
|
||||
/**
|
||||
* @brief The SqlDatabase class
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT SqlDatabase
|
||||
{
|
||||
Q_DISABLE_COPY(SqlDatabase)
|
||||
public:
|
||||
explicit SqlDatabase();
|
||||
~SqlDatabase();
|
||||
|
||||
bool isOpen() const;
|
||||
bool openOrCreateReadWrite(const QString &filename);
|
||||
bool openReadOnly(const QString &filename);
|
||||
bool transaction();
|
||||
bool commit();
|
||||
void close();
|
||||
QString error() const;
|
||||
sqlite3 *sqliteDb();
|
||||
|
||||
private:
|
||||
enum class CheckDbResult {
|
||||
Ok,
|
||||
CantPrepare,
|
||||
CantExec,
|
||||
NotOk,
|
||||
};
|
||||
|
||||
bool openHelper(const QString &filename, int sqliteFlags);
|
||||
CheckDbResult checkDb();
|
||||
|
||||
sqlite3 *_db;
|
||||
QString _error; // last error string
|
||||
int _errId;
|
||||
|
||||
friend class SqlQuery;
|
||||
QSet<SqlQuery *> _queries;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The SqlQuery class
|
||||
* @ingroup libsync
|
||||
*
|
||||
* There is basically 3 ways to initialize and use a query:
|
||||
*
|
||||
SqlQuery q1;
|
||||
[...]
|
||||
q1.initOrReset(...);
|
||||
q1.bindValue(...);
|
||||
q1.exec(...)
|
||||
*
|
||||
SqlQuery q2(db);
|
||||
q2.prepare(...);
|
||||
[...]
|
||||
q2.reset_and_clear_bindings();
|
||||
q2.bindValue(...);
|
||||
q2.exec(...)
|
||||
*
|
||||
SqlQuery q3("...", db);
|
||||
q3.bindValue(...);
|
||||
q3.exec(...)
|
||||
*
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT SqlQuery
|
||||
{
|
||||
Q_DISABLE_COPY(SqlQuery)
|
||||
public:
|
||||
explicit SqlQuery() = default;
|
||||
explicit SqlQuery(SqlDatabase &db);
|
||||
explicit SqlQuery(const QByteArray &sql, SqlDatabase &db);
|
||||
/**
|
||||
* Prepare the SqlQuery.
|
||||
* If the query was already prepared, this will first call finish(), and re-prepare it.
|
||||
* This function must only be used if the constructor was setting a SqlDatabase
|
||||
*/
|
||||
int prepare(const QByteArray &sql, bool allow_failure = false);
|
||||
|
||||
~SqlQuery();
|
||||
QString error() const;
|
||||
int errorId() const;
|
||||
|
||||
/// Checks whether the value at the given column index is NULL
|
||||
bool nullValue(int index);
|
||||
|
||||
QString stringValue(int index);
|
||||
int intValue(int index);
|
||||
quint64 int64Value(int index);
|
||||
QByteArray baValue(int index);
|
||||
bool isSelect();
|
||||
bool isPragma();
|
||||
bool exec();
|
||||
|
||||
struct NextResult
|
||||
{
|
||||
bool ok = false;
|
||||
bool hasData = false;
|
||||
};
|
||||
NextResult next();
|
||||
|
||||
template <class T>
|
||||
void bindValue(int pos, const T &value)
|
||||
{
|
||||
const auto converted = convertValue(value);
|
||||
if (lcSql().isDebugEnabled() && !_boundValues.isEmpty()) {
|
||||
QString sqlValue;
|
||||
auto stream = QDebug(&sqlValue).noquote().nospace() << '\'' << value;
|
||||
if (typeid(converted) != typeid(value)) {
|
||||
stream << " [" << converted << ']';
|
||||
}
|
||||
stream << '\'';
|
||||
_boundValues[pos - 1].value = sqlValue;
|
||||
}
|
||||
bindValueInternal(pos, QVariant::fromValue(converted));
|
||||
}
|
||||
|
||||
const QByteArray &lastQuery() const;
|
||||
int numRowsAffected();
|
||||
void reset_and_clear_bindings();
|
||||
|
||||
private:
|
||||
template <class T, typename std::enable_if<std::is_enum<T>::value, int>::type = 0>
|
||||
auto convertValue(const T &value)
|
||||
{
|
||||
return static_cast<int>(value);
|
||||
}
|
||||
|
||||
template <class T, typename std::enable_if<!std::is_enum<T>::value, int>::type = 0>
|
||||
auto convertValue(const T &value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
void bindValueInternal(int pos, const QVariant &value);
|
||||
void finish();
|
||||
|
||||
SqlDatabase *_sqldb = nullptr;
|
||||
sqlite3 *_db = nullptr;
|
||||
sqlite3_stmt *_stmt = nullptr;
|
||||
QString _error;
|
||||
int _errId;
|
||||
QByteArray _sql;
|
||||
struct Binding
|
||||
{
|
||||
QRegularExpression name;
|
||||
QString value;
|
||||
};
|
||||
QVector<Binding> _boundValues;
|
||||
|
||||
friend class SqlDatabase;
|
||||
friend class PreparedSqlQueryManager;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#endif // OWNSQL_H
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "pinstate.h"
|
||||
#include "moc_pinstate.cpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
template <>
|
||||
QString Utility::enumToDisplayName(VfsItemAvailability availability)
|
||||
{
|
||||
switch (availability) {
|
||||
case VfsItemAvailability::AlwaysLocal:
|
||||
return QCoreApplication::translate("pinstate", "Always available locally");
|
||||
case VfsItemAvailability::AllHydrated:
|
||||
return QCoreApplication::translate("pinstate", "Currently available locally");
|
||||
case VfsItemAvailability::Mixed:
|
||||
return QCoreApplication::translate("pinstate", "Some available online only");
|
||||
case VfsItemAvailability::AllDehydrated:
|
||||
[[fallthrough]];
|
||||
case VfsItemAvailability::OnlineOnly:
|
||||
return QCoreApplication::translate("pinstate", "Available online only");
|
||||
}
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (C) by Christian Kamm <mail@ckamm.de>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#ifndef PINSTATE_H
|
||||
#define PINSTATE_H
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
#include "utility.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
namespace PinStateEnums {
|
||||
QSFERA_SYNC_EXPORT Q_NAMESPACE
|
||||
|
||||
/** Determines whether items should be available locally permanently or not
|
||||
*
|
||||
* The idea is that files and folders can be marked with the user intent
|
||||
* on availability.
|
||||
*
|
||||
* The Inherited state is used for resetting a pin state to what its
|
||||
* parent path would do.
|
||||
*
|
||||
* The pin state of a directory usually only matters for the initial pin and
|
||||
* hydration state of new remote files. It's perfectly possible for a
|
||||
* AlwaysLocal directory to have only OnlineOnly items. (though setting pin
|
||||
* states is usually done recursively, so one'd need to set the folder to
|
||||
* pinned and then each contained item to unpinned)
|
||||
*
|
||||
* Note: This enum intentionally mimics CF_PIN_STATE of Windows cfapi.
|
||||
*/
|
||||
enum class PinState : uint8_t {
|
||||
/** The pin state is derived from the state of the parent folder.
|
||||
*
|
||||
* For example new remote files start out in this state, following
|
||||
* the state of their parent folder.
|
||||
*
|
||||
* This state is used purely for resetting pin states to their derived
|
||||
* value. The effective state for an item will never be "Inherited".
|
||||
*/
|
||||
Inherited = 0,
|
||||
|
||||
/** The file shall be available and up to date locally.
|
||||
*
|
||||
* Also known as "pinned". Pinned dehydrated files shall be hydrated
|
||||
* as soon as possible.
|
||||
*/
|
||||
AlwaysLocal = 1,
|
||||
|
||||
/** File shall be a dehydrated placeholder, filled on demand.
|
||||
*
|
||||
* Also known as "unpinned". Unpinned hydrated files shall be dehydrated
|
||||
* as soon as possible.
|
||||
*
|
||||
* If a unpinned file becomes hydrated (such as due to an implicit hydration
|
||||
* where the user requested access to the file's data) its pin state changes
|
||||
* to Unspecified.
|
||||
*/
|
||||
OnlineOnly = 2,
|
||||
|
||||
/** The user hasn't made a decision. The client or platform may hydrate or
|
||||
* dehydrate as they see fit.
|
||||
*
|
||||
* New remote files in unspecified directories start unspecified, and
|
||||
* dehydrated (which is an arbitrary decision).
|
||||
*/
|
||||
Unspecified = 3,
|
||||
|
||||
/**
|
||||
* Special case for files that are excluded
|
||||
*/
|
||||
Excluded = 4
|
||||
};
|
||||
Q_ENUM_NS(PinState);
|
||||
|
||||
/** A user-facing version of PinState.
|
||||
*
|
||||
* PinStates communicate availability intent for an item, but particular
|
||||
* situations can get complex: An AlwaysLocal folder can have OnlineOnly
|
||||
* files or directories.
|
||||
*
|
||||
* For users this is condensed to a few useful cases.
|
||||
*
|
||||
* Note that this is only about *intent*. The file could still be out of date,
|
||||
* or not have been synced for other reasons, like errors.
|
||||
*
|
||||
* NOTE: The numerical values and ordering of this enum are relevant.
|
||||
*/
|
||||
enum class VfsItemAvailability : uint8_t {
|
||||
/** The item and all its subitems are hydrated and pinned AlwaysLocal.
|
||||
*
|
||||
* This guarantees that all contents will be kept in sync.
|
||||
*/
|
||||
AlwaysLocal = 0,
|
||||
|
||||
/** The item and all its subitems are hydrated.
|
||||
*
|
||||
* This may change if the platform or client decide to dehydrate items
|
||||
* that have Unspecified pin state.
|
||||
*
|
||||
* A folder with no file contents will have this availability.
|
||||
*/
|
||||
AllHydrated = 1,
|
||||
|
||||
/** There are dehydrated and hydrated items.
|
||||
*
|
||||
* This would happen if a dehydration happens to a Unspecified item that
|
||||
* used to be hydrated.
|
||||
*/
|
||||
Mixed = 2,
|
||||
|
||||
/** There are only dehydrated items but the pin state isn't all OnlineOnly.
|
||||
*/
|
||||
AllDehydrated = 3,
|
||||
|
||||
/** The item and all its subitems are dehydrated and OnlineOnly.
|
||||
*
|
||||
* This guarantees that contents will not take up space.
|
||||
*/
|
||||
OnlineOnly = 4,
|
||||
};
|
||||
Q_ENUM_NS(VfsItemAvailability)
|
||||
}
|
||||
using namespace PinStateEnums;
|
||||
|
||||
template <>
|
||||
QSFERA_SYNC_EXPORT QString Utility::enumToDisplayName(VfsItemAvailability availability);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) by Dominik Schmidt <dschmidt@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 "plugin.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
PluginFactory::~PluginFactory() = default;
|
||||
bool PluginFactory::checkAvailability() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
QString pluginFileName(const QString &type, const QString &name)
|
||||
{
|
||||
return QStringLiteral("QSfera_%2_%3").arg(type, name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) by Dominik Schmidt <dschmidt@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
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/common/result.h"
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class QSFERA_SYNC_EXPORT PluginFactory
|
||||
{
|
||||
public:
|
||||
virtual ~PluginFactory();
|
||||
virtual QObject *create(QObject *parent) = 0;
|
||||
[[nodiscard]] virtual bool checkAvailability() const;
|
||||
|
||||
/**
|
||||
* @param path The path for which the plugin should be prepared
|
||||
* @param accountUuid The UUID of the account for which the plugin should be prepared (might be null during account setup)
|
||||
* @return Nothing or an error string
|
||||
*/
|
||||
[[nodiscard]] virtual Result<void, QString> prepare(const QString &path, const QUuid &accountUuid) const = 0;
|
||||
};
|
||||
|
||||
template <class PluginClass>
|
||||
class DefaultPluginFactory : public PluginFactory
|
||||
{
|
||||
public:
|
||||
QObject *create(QObject *parent) override { return new PluginClass(parent); }
|
||||
};
|
||||
|
||||
/// Return the expected name of a plugin, for use with QPluginLoader
|
||||
QString pluginFileName(const QString &type, const QString &name);
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_INTERFACE(OCC::PluginFactory, "eu.qsfera.PluginFactory")
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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 "preparedsqlquerymanager.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
PreparedSqlQuery::PreparedSqlQuery(SqlQuery *query, bool ok)
|
||||
: _query(query)
|
||||
, _ok(ok)
|
||||
{
|
||||
}
|
||||
|
||||
PreparedSqlQuery::~PreparedSqlQuery()
|
||||
{
|
||||
_query->reset_and_clear_bindings();
|
||||
}
|
||||
|
||||
const PreparedSqlQuery PreparedSqlQueryManager::get(PreparedSqlQueryManager::Key key)
|
||||
{
|
||||
auto &query = _queries[key];
|
||||
OC_ENFORCE(query._stmt);
|
||||
Q_ASSERT(!sqlite3_stmt_busy(query._stmt));
|
||||
return {&query};
|
||||
}
|
||||
|
||||
const PreparedSqlQuery PreparedSqlQueryManager::get(PreparedSqlQueryManager::Key key, const QByteArray &sql, SqlDatabase &db)
|
||||
{
|
||||
auto &query = _queries[key];
|
||||
Q_ASSERT(!sqlite3_stmt_busy(query._stmt));
|
||||
OC_ENFORCE(!query._sqldb || &db == query._sqldb);
|
||||
if (!query._stmt) {
|
||||
query._sqldb = &db;
|
||||
query._db = db.sqliteDb();
|
||||
return {&query, query.prepare(sql) == 0};
|
||||
}
|
||||
return {&query};
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/asserts.h"
|
||||
#include "libsync/qsferasynclib.h"
|
||||
#include "ownsql.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class QSFERA_SYNC_EXPORT PreparedSqlQuery
|
||||
{
|
||||
public:
|
||||
~PreparedSqlQuery();
|
||||
|
||||
operator bool() const { return _ok; }
|
||||
|
||||
SqlQuery *operator->() const
|
||||
{
|
||||
Q_ASSERT(_ok);
|
||||
return _query;
|
||||
}
|
||||
|
||||
SqlQuery &operator*() const &
|
||||
{
|
||||
Q_ASSERT(_ok);
|
||||
return *_query;
|
||||
}
|
||||
|
||||
private:
|
||||
PreparedSqlQuery(SqlQuery *query, bool ok = true);
|
||||
|
||||
SqlQuery *_query;
|
||||
bool _ok;
|
||||
|
||||
friend class PreparedSqlQueryManager;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Manage PreparedSqlQuery
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT PreparedSqlQueryManager
|
||||
{
|
||||
public:
|
||||
enum Key {
|
||||
GetFileRecordQuery,
|
||||
GetFileRecordQueryByInode,
|
||||
GetFileRecordQueryByFileId,
|
||||
GetFilesBelowPathQuery,
|
||||
GetAllFilesQuery,
|
||||
ListFilesInPathQuery,
|
||||
SetFileRecordQuery,
|
||||
SetFileRecordChecksumQuery,
|
||||
GetDownloadInfoQuery,
|
||||
SetDownloadInfoQuery,
|
||||
DeleteDownloadInfoQuery,
|
||||
GetUploadInfoQuery,
|
||||
GetAllUploadInfoQuery,
|
||||
SetUploadInfoQuery,
|
||||
DeleteUploadInfoQuery,
|
||||
DeleteFileRecordPhash,
|
||||
DeleteFileRecordRecursively,
|
||||
GetErrorBlacklistQuery,
|
||||
SetErrorBlacklistQuery,
|
||||
GetSelectiveSyncListQuery,
|
||||
GetChecksumTypeIdQuery,
|
||||
GetChecksumTypeQuery,
|
||||
InsertChecksumTypeQuery,
|
||||
GetDataFingerprintQuery,
|
||||
SetDataFingerprintQuery1,
|
||||
SetDataFingerprintQuery2,
|
||||
GetConflictRecordQuery,
|
||||
SetConflictRecordQuery,
|
||||
DeleteConflictRecordQuery,
|
||||
GetRawPinStateQuery,
|
||||
GetEffectivePinStateQuery,
|
||||
GetSubPinsQuery,
|
||||
CountDehydratedFilesQuery,
|
||||
SetPinStateQuery,
|
||||
WipePinStateQuery,
|
||||
|
||||
GetFileReocrdsWithDirtyPlaceholdersQuery,
|
||||
|
||||
PreparedQueryCount
|
||||
};
|
||||
PreparedSqlQueryManager() = default;
|
||||
/**
|
||||
* The queries are reset in the destructor to prevent wal locks
|
||||
*/
|
||||
const PreparedSqlQuery get(Key key);
|
||||
/**
|
||||
* Prepare the SqlQuery if it was not prepared yet.
|
||||
*/
|
||||
const PreparedSqlQuery get(Key key, const QByteArray &sql, SqlDatabase &db);
|
||||
|
||||
private:
|
||||
SqlQuery _queries[PreparedQueryCount];
|
||||
Q_DISABLE_COPY(PreparedSqlQueryManager)
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.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 "remotepermissions.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
static const char letters[] = " WDNVCKRSMmz";
|
||||
|
||||
|
||||
template <typename Char>
|
||||
void RemotePermissions::fromArray(const Char *p)
|
||||
{
|
||||
_value = notNullMask;
|
||||
if (!p)
|
||||
return;
|
||||
while (*p) {
|
||||
if (auto res = std::strchr(letters, static_cast<char>(*p)))
|
||||
_value |= (1 << (res - letters));
|
||||
++p;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray RemotePermissions::toDbValue() const
|
||||
{
|
||||
QByteArray result;
|
||||
if (isNull())
|
||||
return result;
|
||||
result.reserve(PermissionsCount);
|
||||
for (uint i = 1; i <= PermissionsCount; ++i) {
|
||||
if (_value & (1 << i))
|
||||
result.append(letters[i]);
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
// Make sure it is not empty so we can differentiate null and empty permissions
|
||||
result.append(' ');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString RemotePermissions::toString() const
|
||||
{
|
||||
return QString::fromUtf8(toDbValue());
|
||||
}
|
||||
|
||||
RemotePermissions RemotePermissions::fromDbValue(const QByteArray &value)
|
||||
{
|
||||
if (value.isEmpty())
|
||||
return RemotePermissions();
|
||||
RemotePermissions perm;
|
||||
perm.fromArray(value.constData());
|
||||
return perm;
|
||||
}
|
||||
|
||||
RemotePermissions RemotePermissions::fromServerString(const QString &value)
|
||||
{
|
||||
RemotePermissions perm;
|
||||
perm.fromArray(value.utf16());
|
||||
return perm;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.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
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
#include <QDebug>
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* Class that store in a memory efficient way the remote permission
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT RemotePermissions
|
||||
{
|
||||
private:
|
||||
// The first bit tells if the value is set or not
|
||||
// The remaining bits correspond to know if the value is set
|
||||
quint16 _value = 0;
|
||||
static constexpr int notNullMask = 0x1;
|
||||
|
||||
template <typename Char> // can be 'char' or 'ushort' if conversion from QString
|
||||
void fromArray(const Char *p);
|
||||
|
||||
public:
|
||||
enum Permissions {
|
||||
CanWrite = 1, // W
|
||||
CanDelete = 2, // D
|
||||
CanRename = 3, // N
|
||||
CanMove = 4, // V
|
||||
CanAddFile = 5, // C
|
||||
CanAddSubDirectories = 6, // K
|
||||
CanReshare = 7, // R
|
||||
// Note: on the server, this means SharedWithMe, but in discoveryphase.cpp we also set
|
||||
// this permission when the server reports the any "share-types"
|
||||
IsShared = 8, // S
|
||||
IsMounted = 9, // M
|
||||
IsMountedSub = 10, // m (internal: set if the parent dir has IsMounted)
|
||||
DeprecatedRemoved = 11, // z (Deprecated, don't use)
|
||||
|
||||
// Note: when adding support for more permissions, we need to invalid the cache in the database.
|
||||
// (by setting forceRemoteDiscovery in SyncJournalDb::checkConnect)
|
||||
PermissionsCount = DeprecatedRemoved
|
||||
};
|
||||
|
||||
/// null permissions
|
||||
RemotePermissions() = default;
|
||||
|
||||
/// array with one character per permission, "" is null, " " is non-null but empty
|
||||
QByteArray toDbValue() const;
|
||||
|
||||
/// output for display purposes, no defined format (same as toDbValue in practice)
|
||||
QString toString() const;
|
||||
|
||||
/// read value that was written with toDbValue()
|
||||
static RemotePermissions fromDbValue(const QByteArray &);
|
||||
|
||||
/// read a permissions string received from the server, never null
|
||||
static RemotePermissions fromServerString(const QString &);
|
||||
|
||||
bool hasPermission(Permissions p) const { return _value & (1 << static_cast<int>(p)); }
|
||||
void setPermission(Permissions p) { _value |= (1 << static_cast<int>(p)) | notNullMask; }
|
||||
void unsetPermission(Permissions p) { _value &= ~(1 << static_cast<int>(p)); }
|
||||
|
||||
bool isNull() const { return !(_value & notNullMask); }
|
||||
friend bool operator==(RemotePermissions a, RemotePermissions b) { return a._value == b._value; }
|
||||
friend bool operator!=(RemotePermissions a, RemotePermissions b) { return !(a == b); }
|
||||
|
||||
friend QDebug operator<<(QDebug &dbg, RemotePermissions p) { return dbg << p.toString(); }
|
||||
};
|
||||
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
Q_DECLARE_METATYPE(OCC::RemotePermissions)
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "restartmanager.h"
|
||||
|
||||
#include "utility.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFileInfo>
|
||||
#include <QLoggingCategory>
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
|
||||
Q_LOGGING_CATEGORY(lcRestart, "sync.restartmanager", QtInfoMsg)
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
RestartManager *RestartManager::_instance = nullptr;
|
||||
|
||||
RestartManager::RestartManager(std::function<int(int, char **)> &&main)
|
||||
: _main(main)
|
||||
{
|
||||
Q_ASSERT(!_instance);
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
RestartManager::~RestartManager()
|
||||
{
|
||||
if (!_applicationToRestart.isEmpty()) {
|
||||
QProcess process;
|
||||
process.setProgram(_applicationToRestart);
|
||||
process.setArguments(_args);
|
||||
qint64 pid;
|
||||
qCDebug(lcRestart) << u"Detaching" << _applicationToRestart << _args;
|
||||
if (process.startDetached(&pid)) {
|
||||
qCDebug(lcRestart) << u"Successfully restarted. New process PID" << pid;
|
||||
} else {
|
||||
qCCritical(lcRestart) << u"Failed to restart" << process.error() << process.errorString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int RestartManager::exec(int argc, char **argv) const
|
||||
{
|
||||
return _main(argc, argv);
|
||||
}
|
||||
|
||||
void RestartManager::requestRestart()
|
||||
{
|
||||
Q_ASSERT(_instance);
|
||||
qCInfo(lcRestart) << u"Restarting application with PID" << QCoreApplication::applicationPid();
|
||||
|
||||
QString pathToLaunch = QCoreApplication::applicationFilePath();
|
||||
#ifdef Q_OS_LINUX
|
||||
if (Utility::runningInAppImage()) {
|
||||
pathToLaunch = Utility::appImageLocation();
|
||||
}
|
||||
#endif
|
||||
_instance->_applicationToRestart = QFileInfo(pathToLaunch).absoluteFilePath();
|
||||
// remove arg0
|
||||
_instance->_args = QCoreApplication::arguments().sliced(1);
|
||||
QTimer::singleShot(0, QCoreApplication::instance(), &QCoreApplication::quit);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace OCC {
|
||||
class QSFERA_SYNC_EXPORT RestartManager
|
||||
{
|
||||
public:
|
||||
RestartManager(std::function<int(int, char **)> &&main);
|
||||
|
||||
~RestartManager();
|
||||
|
||||
int exec(int argc, char **argv) const;
|
||||
|
||||
static void requestRestart();
|
||||
|
||||
private:
|
||||
static RestartManager *_instance;
|
||||
|
||||
std::function<int(int, char **)> _main;
|
||||
|
||||
QString _applicationToRestart;
|
||||
QStringList _args;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "asserts.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* A Result of type T, or an Error
|
||||
*/
|
||||
template <typename T, typename Error>
|
||||
class Result
|
||||
{
|
||||
union {
|
||||
T _result;
|
||||
Error _error;
|
||||
};
|
||||
bool _isError;
|
||||
|
||||
public:
|
||||
Result(T value)
|
||||
: _result(std::move(value))
|
||||
, _isError(false)
|
||||
{
|
||||
static_assert(!std::is_same<T, bool>::value, "Bool is not supported as this class overrides the bool operator");
|
||||
}
|
||||
// TODO: This doesn't work if T and Error are too similar
|
||||
Result(Error error)
|
||||
: _error(std::move(error))
|
||||
, _isError(true)
|
||||
{
|
||||
}
|
||||
|
||||
Result(Result &&other)
|
||||
: _isError(other._isError)
|
||||
{
|
||||
if (_isError) {
|
||||
new (&_error) Error(std::move(other._error));
|
||||
} else {
|
||||
new (&_result) T(std::move(other._result));
|
||||
}
|
||||
}
|
||||
|
||||
Result(const Result &other)
|
||||
: _isError(other._isError)
|
||||
{
|
||||
if (_isError) {
|
||||
new (&_error) Error(other._error);
|
||||
} else {
|
||||
new (&_result) T(other._result);
|
||||
}
|
||||
}
|
||||
|
||||
Result &operator=(Result &&other)
|
||||
{
|
||||
if (&other != this) {
|
||||
_isError = other._isError;
|
||||
if (_isError) {
|
||||
new (&_error) Error(std::move(other._error));
|
||||
} else {
|
||||
new (&_result) T(std::move(other._result));
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Result &operator=(const Result &other)
|
||||
{
|
||||
if (&other != this) {
|
||||
_isError = other._isError;
|
||||
if (_isError) {
|
||||
new (&_error) Error(other._error);
|
||||
} else {
|
||||
new (&_result) T(other._result);
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~Result()
|
||||
{
|
||||
if (_isError)
|
||||
_error.~Error();
|
||||
else
|
||||
_result.~T();
|
||||
}
|
||||
|
||||
explicit operator bool() const { return !_isError; }
|
||||
|
||||
const T &operator*() const &
|
||||
{
|
||||
OC_ASSERT(!_isError);
|
||||
return _result;
|
||||
}
|
||||
|
||||
T operator*() &&
|
||||
{
|
||||
OC_ASSERT(!_isError);
|
||||
return std::move(_result);
|
||||
}
|
||||
|
||||
const T *operator->() const
|
||||
{
|
||||
OC_ASSERT(!_isError);
|
||||
return &_result;
|
||||
}
|
||||
|
||||
const T &get() const
|
||||
{
|
||||
OC_ASSERT(!_isError);
|
||||
return _result;
|
||||
}
|
||||
|
||||
const Error &error() const &
|
||||
{
|
||||
OC_ASSERT(_isError);
|
||||
return _error;
|
||||
}
|
||||
Error error() &&
|
||||
{
|
||||
OC_ASSERT(_isError);
|
||||
return std::move(_error);
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
struct NoResultData
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
template <typename Error>
|
||||
class Result<void, Error> : public Result<detail::NoResultData, Error>
|
||||
{
|
||||
public:
|
||||
using Result<detail::NoResultData, Error>::Result;
|
||||
Result()
|
||||
: Result(detail::NoResultData{})
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
struct OptionalNoErrorData
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
class Optional : public Result<T, detail::OptionalNoErrorData>
|
||||
{
|
||||
public:
|
||||
using Result<T, detail::OptionalNoErrorData>::Result;
|
||||
|
||||
Optional()
|
||||
: Optional(detail::OptionalNoErrorData{})
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "syncfilestatus.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
namespace OCC {
|
||||
SyncFileStatus::SyncFileStatus()
|
||||
: _tag(StatusNone)
|
||||
, _shared(false)
|
||||
{
|
||||
}
|
||||
|
||||
SyncFileStatus::SyncFileStatus(SyncFileStatusTag tag)
|
||||
: _tag(tag)
|
||||
, _shared(false)
|
||||
{
|
||||
}
|
||||
|
||||
void SyncFileStatus::set(SyncFileStatusTag tag)
|
||||
{
|
||||
_tag = tag;
|
||||
}
|
||||
|
||||
SyncFileStatus::SyncFileStatusTag SyncFileStatus::tag() const
|
||||
{
|
||||
return _tag;
|
||||
}
|
||||
|
||||
void SyncFileStatus::setShared(bool isShared)
|
||||
{
|
||||
_shared = isShared;
|
||||
}
|
||||
|
||||
bool SyncFileStatus::shared() const
|
||||
{
|
||||
return _shared;
|
||||
}
|
||||
|
||||
QString SyncFileStatus::toSocketAPIString() const
|
||||
{
|
||||
QString statusString;
|
||||
bool canBeShared = true;
|
||||
|
||||
switch (_tag) {
|
||||
case StatusNone:
|
||||
statusString = QStringLiteral("NOP");
|
||||
canBeShared = false;
|
||||
break;
|
||||
case StatusSync:
|
||||
statusString = QStringLiteral("SYNC");
|
||||
break;
|
||||
case StatusWarning:
|
||||
// The protocol says IGNORE, but all implementations show a yellow warning sign.
|
||||
statusString = QStringLiteral("IGNORE");
|
||||
break;
|
||||
case StatusUpToDate:
|
||||
statusString = QStringLiteral("OK");
|
||||
break;
|
||||
case StatusError:
|
||||
statusString = QStringLiteral("ERROR");
|
||||
break;
|
||||
case StatusExcluded:
|
||||
// The protocol says IGNORE, but all implementations show a yellow warning sign.
|
||||
statusString = QStringLiteral("IGNORE");
|
||||
break;
|
||||
}
|
||||
if (canBeShared && _shared) {
|
||||
statusString += QLatin1String("+SWM");
|
||||
}
|
||||
|
||||
return statusString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
QDebug &operator<<(QDebug &debug, const OCC::SyncFileStatus &item)
|
||||
{
|
||||
QDebugStateSaver saver(debug);
|
||||
debug.setAutoInsertSpaces(false);
|
||||
debug << u"OCC::SyncFileStatus(shared=" << item.shared() << u", tag=" << item.tag() << u")";
|
||||
return debug;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#ifndef SYNCFILESTATUS_H
|
||||
#define SYNCFILESTATUS_H
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* @brief The SyncFileStatus class
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT SyncFileStatus
|
||||
{
|
||||
Q_GADGET
|
||||
public:
|
||||
enum SyncFileStatusTag : uint8_t {
|
||||
StatusNone,
|
||||
StatusSync,
|
||||
StatusWarning,
|
||||
StatusUpToDate,
|
||||
StatusError,
|
||||
StatusExcluded,
|
||||
};
|
||||
Q_ENUM(SyncFileStatusTag);
|
||||
|
||||
SyncFileStatus();
|
||||
SyncFileStatus(SyncFileStatusTag);
|
||||
|
||||
void set(SyncFileStatusTag tag);
|
||||
SyncFileStatusTag tag() const;
|
||||
|
||||
void setShared(bool isShared);
|
||||
bool shared() const;
|
||||
|
||||
QString toSocketAPIString() const;
|
||||
|
||||
private:
|
||||
SyncFileStatusTag _tag;
|
||||
bool _shared;
|
||||
};
|
||||
|
||||
inline bool operator==(const SyncFileStatus &a, const SyncFileStatus &b)
|
||||
{
|
||||
return a.tag() == b.tag() && a.shared() == b.shared();
|
||||
}
|
||||
|
||||
inline bool operator!=(const SyncFileStatus &a, const SyncFileStatus &b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
}
|
||||
QSFERA_SYNC_EXPORT QDebug &operator<<(QDebug &debug, const OCC::SyncFileStatus &item);
|
||||
|
||||
Q_DECLARE_METATYPE(OCC::SyncFileStatus)
|
||||
|
||||
#endif // SYNCFILESTATUS_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef SYNCJOURNALDB_H
|
||||
#define SYNCJOURNALDB_H
|
||||
|
||||
|
||||
#include "libsync/common/checksumalgorithms.h"
|
||||
#include "libsync/common/ownsql.h"
|
||||
#include "libsync/common/preparedsqlquerymanager.h"
|
||||
#include "libsync/common/result.h"
|
||||
#include "libsync/common/syncjournalfilerecord.h"
|
||||
#include "libsync/common/utility.h"
|
||||
|
||||
#include <QMutex>
|
||||
|
||||
namespace OCC {
|
||||
class SyncJournalFileRecord;
|
||||
|
||||
/**
|
||||
* @brief Class that handles the sync database
|
||||
*
|
||||
* This class is thread safe. All public functions lock the mutex.
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT SyncJournalDb : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SyncJournalDb(const QString &dbFilePath, QObject *parent = nullptr);
|
||||
~SyncJournalDb() override;
|
||||
|
||||
/// Create a journal path for a specific configuration
|
||||
static QString makeDbName(const QString &localPath, const QString &infix = QStringLiteral("journal"));
|
||||
|
||||
static bool dbIsTooNewForClient(const QString &dbFilePath);
|
||||
|
||||
// To verify that the record could be found check with SyncJournalFileRecord::isValid()
|
||||
|
||||
/**
|
||||
* Returns the file record for the given filename.
|
||||
* @param filename The filename, which must be a relative path (relative to the sync root).
|
||||
* Passing an absolute path is not allowed and will not yield a result.
|
||||
*/
|
||||
SyncJournalFileRecord getFileRecord(const QString &filename);
|
||||
SyncJournalFileRecord getFileRecordByInode(quint64 inode);
|
||||
bool getFileRecordsByFileId(const QByteArray &fileId, const std::function<void(const SyncJournalFileRecord &)> &rowCallback);
|
||||
bool getFilesBelowPath(const QString &path, const std::function<void(const SyncJournalFileRecord &)> &rowCallback);
|
||||
bool listFilesInPath(const QString &path, const std::function<void(const SyncJournalFileRecord &)> &rowCallback);
|
||||
const QVector<SyncJournalFileRecord> getFileRecordsWithDirtyPlaceholders() const;
|
||||
Result<void, QString> setFileRecord(const SyncJournalFileRecord &record);
|
||||
|
||||
bool deleteFileRecord(const QString &filename, bool recursively = false);
|
||||
bool updateFileRecordChecksum(const QString &filename, const QByteArray &contentChecksum, CheckSums::Algorithm contentChecksumType);
|
||||
|
||||
/// Return value for hasHydratedOrDehydratedFiles()
|
||||
struct HasHydratedDehydrated
|
||||
{
|
||||
bool hasHydrated = false;
|
||||
bool hasDehydrated = false;
|
||||
};
|
||||
|
||||
/** Returns whether the item or any subitems are dehydrated */
|
||||
Optional<HasHydratedDehydrated> hasHydratedOrDehydratedFiles(const QByteArray &filename);
|
||||
|
||||
bool exists();
|
||||
void walCheckpoint();
|
||||
|
||||
QString databaseFilePath() const;
|
||||
|
||||
void setErrorBlacklistEntry(const SyncJournalErrorBlacklistRecord &item);
|
||||
void wipeErrorBlacklistEntry(const QString &relativeFile);
|
||||
void wipeErrorBlacklistEntry(const QString &relativeFile, SyncJournalErrorBlacklistRecord::Category category);
|
||||
void wipeErrorBlacklistCategory(SyncJournalErrorBlacklistRecord::Category category);
|
||||
int wipeErrorBlacklist();
|
||||
int errorBlackListEntryCount();
|
||||
|
||||
struct DownloadInfo
|
||||
{
|
||||
DownloadInfo()
|
||||
: _errorCount(0)
|
||||
, _valid(false)
|
||||
{
|
||||
}
|
||||
QString _tmpfile;
|
||||
QByteArray _etag;
|
||||
int _errorCount;
|
||||
bool _valid;
|
||||
};
|
||||
struct UploadInfo
|
||||
{
|
||||
bool _valid = false;
|
||||
|
||||
qint64 _size = 0;
|
||||
qint64 _modtime = 0;
|
||||
QByteArray _contentChecksum;
|
||||
QUrl _url; // upload url (tus)
|
||||
QString _path; // stored as utf16, used in local discovery
|
||||
|
||||
bool validate(qint64 size, qint64 modtime, const QByteArray &checksum) const
|
||||
{
|
||||
Q_ASSERT(!checksum.isEmpty());
|
||||
Q_ASSERT(!_valid || !_contentChecksum.isEmpty());
|
||||
return _valid && _size == size && _modtime == modtime && _contentChecksum == checksum;
|
||||
}
|
||||
};
|
||||
|
||||
DownloadInfo getDownloadInfo(const QString &file);
|
||||
void setDownloadInfo(const QString &file, const DownloadInfo &i);
|
||||
QVector<DownloadInfo> getAndDeleteStaleDownloadInfos(const QSet<QString> &keep);
|
||||
int downloadInfoCount();
|
||||
|
||||
UploadInfo getUploadInfo(const QString &file);
|
||||
std::vector<UploadInfo> getUploadInfos();
|
||||
|
||||
void setUploadInfo(const QString &file, const UploadInfo &i);
|
||||
|
||||
bool deleteStaleUploadInfos(const QSet<QString> &keep);
|
||||
|
||||
SyncJournalErrorBlacklistRecord errorBlacklistEntry(const QString &);
|
||||
bool deleteStaleErrorBlacklistEntries(const QSet<QString> &keep);
|
||||
|
||||
void avoidRenamesOnNextSync(const QString &path) { avoidRenamesOnNextSync(path.toUtf8()); }
|
||||
void avoidRenamesOnNextSync(const QByteArray &path);
|
||||
|
||||
enum SelectiveSyncListType {
|
||||
/** The black list is the list of folders that are unselected in the selective sync dialog.
|
||||
* For the sync engine, those folders are considered as if they were not there, so the local
|
||||
* folders will be deleted */
|
||||
SelectiveSyncBlackList = 1,
|
||||
/** When a shared folder has a size bigger than a configured size, it is by default not sync'ed
|
||||
* Unless it is in the white list, in which case the folder is sync'ed and all its children.
|
||||
* If a folder is both on the black and the white list, the black list wins */
|
||||
SelectiveSyncWhiteList = 2,
|
||||
/** List of big sync folders that have not been confirmed by the user yet and that the UI
|
||||
* should notify about */
|
||||
SelectiveSyncUndecidedList = 3
|
||||
};
|
||||
Q_ENUM(SelectiveSyncListType)
|
||||
|
||||
/* return the specified list from the database */
|
||||
QSet<QString> getSelectiveSyncList(SelectiveSyncListType type, bool *ok);
|
||||
/* Write the selective sync list (remove all other entries of that list */
|
||||
void setSelectiveSyncList(SelectiveSyncListType type, const QSet<QString> &list);
|
||||
|
||||
/**
|
||||
* Make sure that on the next sync fileName and its parents are discovered from the server.
|
||||
*
|
||||
* That means its metadata and, if it's a directory, its direct contents.
|
||||
*
|
||||
* Specifically, etag (md5 field) of fileName and all its parents are set to _invalid_.
|
||||
* That causes a metadata difference and a resulting discovery from the remote for the
|
||||
* affected folders.
|
||||
*
|
||||
* Since folders in the selective sync list will not be rediscovered (csync_ftw,
|
||||
* _csync_detect_update skip them), the _invalid_ marker will stay. And any
|
||||
* child items in the db will be ignored when reading a remote tree from the database.
|
||||
*
|
||||
* Any setFileRecord() call to affected directories before the next sync run will be
|
||||
* adjusted to retain the invalid etag via _etagStorageFilter.
|
||||
*/
|
||||
void schedulePathForRemoteDiscovery(const QString &fileName) { schedulePathForRemoteDiscovery(fileName.toUtf8()); }
|
||||
void schedulePathForRemoteDiscovery(const QByteArray &fileName);
|
||||
|
||||
/**
|
||||
* Wipe _etagStorageFilter. Also done implicitly on close().
|
||||
*/
|
||||
void clearEtagStorageFilter();
|
||||
|
||||
/**
|
||||
* Ensures full remote discovery happens on the next sync.
|
||||
*
|
||||
* Equivalent to calling schedulePathForRemoteDiscovery() for all files.
|
||||
*/
|
||||
void forceRemoteDiscoveryNextSync();
|
||||
|
||||
/* Because sqlite transactions are really slow, we encapsulate everything in big transactions
|
||||
* Commit will actually commit the transaction and create a new one.
|
||||
*/
|
||||
void commit(const QString &context, bool startTrans = true);
|
||||
void commitIfNeededAndStartNewTransaction(const QString &context);
|
||||
|
||||
/** Open the db if it isn't already.
|
||||
*
|
||||
* This usually creates some temporary files next to the db file, like
|
||||
* $dbfile-shm or $dbfile-wal.
|
||||
*
|
||||
* returns true if it could be openend or is currently opened.
|
||||
*/
|
||||
bool open();
|
||||
|
||||
/** Returns whether the db is currently openend. */
|
||||
bool isOpen() const;
|
||||
|
||||
/** Close the database */
|
||||
void close();
|
||||
|
||||
/**
|
||||
* allow to reopen/recreate the db after it was closed (unit tests)
|
||||
* This is usually not allowed to prevent accidential recreation of db.
|
||||
*/
|
||||
void allowReopen();
|
||||
|
||||
/**
|
||||
* Returns the checksum type for an id.
|
||||
*/
|
||||
QByteArray getChecksumType(int checksumTypeId);
|
||||
|
||||
// Conflict record functions
|
||||
|
||||
/// Store a new or updated record in the database
|
||||
void setConflictRecord(const ConflictRecord &record);
|
||||
|
||||
/// Retrieve a conflict record by path of the file with the conflict tag
|
||||
ConflictRecord conflictRecord(const QByteArray &path);
|
||||
|
||||
/// Delete a conflict record by path of the file with the conflict tag
|
||||
void deleteConflictRecord(const QByteArray &path);
|
||||
|
||||
/// Return all paths of files with a conflict tag in the name and records in the db
|
||||
QByteArrayList conflictRecordPaths();
|
||||
|
||||
/** Find the base name for a conflict file name, using journal or name pattern
|
||||
*
|
||||
* The path must by sync-folder relative.
|
||||
*
|
||||
* Will return an empty string if it's not even a conflict file by pattern.
|
||||
*/
|
||||
QString conflictFileBaseName(const QString &conflictName);
|
||||
|
||||
/**
|
||||
* Delete any file entry. This will force the next sync to re-sync everything as if it was new,
|
||||
* restoring everyfile on every remote. If a file is there both on the client and server side,
|
||||
* it will be a conflict that will be automatically resolved if the file is the same.
|
||||
*/
|
||||
void clearFileTable();
|
||||
|
||||
/**
|
||||
* Only used for auto-test:
|
||||
* when positive, will decrease the counter for every database operation.
|
||||
* reaching 0 makes the operation fails
|
||||
*/
|
||||
int autotestFailCounter = -1;
|
||||
|
||||
private:
|
||||
int getFileRecordCount();
|
||||
bool updateDatabaseStructure();
|
||||
bool updateMetadataTableStructure();
|
||||
bool updateErrorBlacklistTableStructure();
|
||||
bool sqlFail(const QString &log, const SqlQuery &query);
|
||||
void commitInternal(const QString &context, bool startTrans = true);
|
||||
void startTransaction();
|
||||
void commitTransaction();
|
||||
QVector<QByteArray> tableColumns(const QByteArray &table);
|
||||
bool checkConnect();
|
||||
|
||||
bool createUploadInfo();
|
||||
|
||||
// Same as forceRemoteDiscoveryNextSync but without acquiring the lock
|
||||
void forceRemoteDiscoveryNextSyncLocked();
|
||||
|
||||
// Returns the integer id of the checksum type
|
||||
//
|
||||
// Returns 0 on failure and for empty checksum types.
|
||||
int mapChecksumType(CheckSums::Algorithm checksumType);
|
||||
|
||||
SqlDatabase _db;
|
||||
QString _dbFile;
|
||||
mutable QRecursiveMutex _mutex; // Public functions are protected with the mutex.
|
||||
QMap<CheckSums::Algorithm, int> _checksymTypeCache;
|
||||
int _transaction;
|
||||
bool _metadataTableIsEmpty;
|
||||
|
||||
/* Storing etags to these folders, or their parent folders, is filtered out.
|
||||
*
|
||||
* When schedulePathForRemoteDiscovery() is called some etags to _invalid_ in the
|
||||
* database. If this is done during a sync run, a later propagation job might
|
||||
* undo that by writing the correct etag to the database instead. This filter
|
||||
* will prevent this write and instead guarantee the _invalid_ etag stays in
|
||||
* place.
|
||||
*
|
||||
* The list is cleared on close() (end of sync run) and explicitly with
|
||||
* clearEtagStorageFilter() (start of sync run).
|
||||
*
|
||||
* The contained paths have a trailing /.
|
||||
*/
|
||||
QList<QByteArray> _etagStorageFilter;
|
||||
|
||||
/** The journal mode to use for the db.
|
||||
*
|
||||
* Typically WAL initially, but may be set to other modes via environment
|
||||
* variable, for specific filesystems, or when WAL fails in a particular way.
|
||||
*/
|
||||
QByteArray _journalMode;
|
||||
|
||||
mutable PreparedSqlQueryManager _queryManager;
|
||||
|
||||
/**
|
||||
* Whether the db was already closed, prevent recreation
|
||||
*/
|
||||
bool _closed = false;
|
||||
};
|
||||
|
||||
bool QSFERA_SYNC_EXPORT operator==(const SyncJournalDb::DownloadInfo &lhs, const SyncJournalDb::DownloadInfo &rhs);
|
||||
bool QSFERA_SYNC_EXPORT operator==(const SyncJournalDb::UploadInfo &lhs, const SyncJournalDb::UploadInfo &rhs);
|
||||
|
||||
} // namespace OCC
|
||||
#endif // SYNCJOURNALDB_H
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* 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 "libsync/common/syncjournalfilerecord.h"
|
||||
|
||||
#include "libsync/common/utility.h"
|
||||
#include "libsync/filesystem.h"
|
||||
#include "libsync/syncfileitem.h"
|
||||
|
||||
Q_LOGGING_CATEGORY(lcSyncJournalFileRecord, "sync.syncjournalfilerecord", QtInfoMsg)
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
|
||||
class OCC::SyncJournalFileRecordData : public QSharedData
|
||||
{
|
||||
public:
|
||||
SyncJournalFileRecordData() = default;
|
||||
~SyncJournalFileRecordData() = default;
|
||||
|
||||
QString _path;
|
||||
quint64 _inode = 0;
|
||||
std::optional<qint64> _modtime;
|
||||
ItemType _type = ItemTypeUnsupported;
|
||||
QString _etag;
|
||||
QByteArray _fileId;
|
||||
qint64 _fileSize = 0;
|
||||
RemotePermissions _remotePerm;
|
||||
bool _serverHasIgnoredFiles = false;
|
||||
bool _hasDirtyPlaceholder = false;
|
||||
QByteArray _checksumHeader;
|
||||
|
||||
QString _error;
|
||||
};
|
||||
|
||||
bool SyncJournalErrorBlacklistRecord::isValid() const
|
||||
{
|
||||
return !_file.isEmpty() && (!_lastTryEtag.isEmpty() || _lastTryModtime != 0) && _lastTryTime > 0;
|
||||
}
|
||||
|
||||
SyncJournalFileRecord::SyncJournalFileRecord()
|
||||
: d([] {
|
||||
static auto nullData = QExplicitlySharedDataPointer{new SyncJournalFileRecordData{}};
|
||||
return nullData;
|
||||
}())
|
||||
{
|
||||
}
|
||||
SyncJournalFileRecord::SyncJournalFileRecord(const QString &path, ItemType type)
|
||||
: d(new SyncJournalFileRecordData)
|
||||
{
|
||||
d->_path = path;
|
||||
d->_type = type;
|
||||
}
|
||||
|
||||
bool SyncJournalFileRecord::validateRecord()
|
||||
{
|
||||
Q_ASSERT(d != SyncJournalFileRecord().d);
|
||||
Q_ASSERT(!remotePerm().isNull());
|
||||
Q_ASSERT(inode() != 0);
|
||||
Q_ASSERT(d->_modtime.has_value());
|
||||
Q_ASSERT(!fileId().isEmpty());
|
||||
Q_ASSERT(type() != ItemTypeUnsupported);
|
||||
return true;
|
||||
}
|
||||
|
||||
SyncJournalFileRecord::SyncJournalFileRecord(const QString &error)
|
||||
: d(new SyncJournalFileRecordData)
|
||||
{
|
||||
d->_error = error;
|
||||
}
|
||||
|
||||
SyncJournalFileRecord::~SyncJournalFileRecord() = default;
|
||||
SyncJournalFileRecord::SyncJournalFileRecord(const SyncJournalFileRecord &other) = default;
|
||||
SyncJournalFileRecord &SyncJournalFileRecord::operator=(const SyncJournalFileRecord &other) = default;
|
||||
|
||||
QByteArray SyncJournalFileRecord::query()
|
||||
{
|
||||
return QByteArrayLiteral("SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize,"
|
||||
" ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum,"
|
||||
" hasDirtyPlaceholder"
|
||||
" FROM metadata"
|
||||
" LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id ");
|
||||
}
|
||||
|
||||
SyncJournalFileRecord SyncJournalFileRecord::fromSqlQuery(OCC::SqlQuery &query)
|
||||
{
|
||||
// keep in sync with query
|
||||
SyncJournalFileRecord rec(QString::fromUtf8(query.baValue(0)), static_cast<ItemType>(query.intValue(3)));
|
||||
rec.d->_inode = query.int64Value(1);
|
||||
rec.d->_modtime = query.int64Value(2);
|
||||
rec.d->_etag = QString::fromUtf8(query.baValue(4));
|
||||
rec.d->_fileId = query.baValue(5);
|
||||
rec.d->_remotePerm = OCC::RemotePermissions::fromDbValue(query.baValue(6));
|
||||
rec.d->_fileSize = query.int64Value(7);
|
||||
rec.d->_serverHasIgnoredFiles = (query.intValue(8) > 0);
|
||||
rec.d->_checksumHeader = query.baValue(9);
|
||||
rec.d->_hasDirtyPlaceholder = query.intValue(10);
|
||||
qCDebug(lcSyncJournalFileRecord) << u"Restored from db:" << rec;
|
||||
Q_ASSERT(rec.validateRecord());
|
||||
return rec;
|
||||
}
|
||||
|
||||
SyncJournalFileRecord SyncJournalFileRecord::fromSyncFileItem(const SyncFileItem &syncFile)
|
||||
{
|
||||
SyncJournalFileRecord rec(syncFile.destination(), syncFile._type);
|
||||
rec.d->_modtime = syncFile._modtime;
|
||||
|
||||
// Some types should never be written to the database when propagation completes
|
||||
if (rec.d->_type == ItemTypeVirtualFileDownload)
|
||||
rec.d->_type = ItemTypeFile;
|
||||
if (rec.d->_type == ItemTypeVirtualFileDehydration)
|
||||
rec.d->_type = ItemTypeVirtualFile;
|
||||
|
||||
rec.d->_etag = syncFile._etag;
|
||||
rec.d->_fileId = syncFile._fileId;
|
||||
rec.d->_fileSize = syncFile._size;
|
||||
rec.d->_inode = syncFile._inode;
|
||||
rec.d->_remotePerm = syncFile._remotePerm;
|
||||
rec.d->_serverHasIgnoredFiles = syncFile._serverHasIgnoredFiles;
|
||||
rec.d->_checksumHeader = syncFile._checksumHeader;
|
||||
Q_ASSERT(rec.validateRecord());
|
||||
|
||||
return rec;
|
||||
}
|
||||
|
||||
bool SyncJournalFileRecord::isValid() const
|
||||
{
|
||||
return !hasError() && !d->_path.isEmpty();
|
||||
}
|
||||
|
||||
QDateTime SyncJournalFileRecord::modDateTime() const
|
||||
{
|
||||
return Utility::qDateTimeFromTime_t(modtime());
|
||||
}
|
||||
|
||||
bool SyncJournalFileRecord::isDirectory() const
|
||||
{
|
||||
return d->_type == ItemTypeDirectory;
|
||||
}
|
||||
|
||||
bool SyncJournalFileRecord::isFile() const
|
||||
{
|
||||
return d->_type == ItemTypeFile || d->_type == ItemTypeVirtualFileDehydration;
|
||||
}
|
||||
|
||||
bool SyncJournalFileRecord::isVirtualFile() const
|
||||
{
|
||||
return d->_type == ItemTypeVirtualFile || d->_type == ItemTypeVirtualFileDownload;
|
||||
}
|
||||
QString SyncJournalFileRecord::path() const
|
||||
{
|
||||
return d->_path;
|
||||
}
|
||||
|
||||
QString SyncJournalFileRecord::name() const
|
||||
{
|
||||
return path().mid(path().lastIndexOf('/'_L1) + 1);
|
||||
}
|
||||
QString SyncJournalFileRecord::etag() const
|
||||
{
|
||||
return d->_etag;
|
||||
}
|
||||
QByteArray SyncJournalFileRecord::fileId() const
|
||||
{
|
||||
return d->_fileId;
|
||||
}
|
||||
RemotePermissions SyncJournalFileRecord::remotePerm() const
|
||||
{
|
||||
return d->_remotePerm;
|
||||
}
|
||||
|
||||
QByteArray SyncJournalFileRecord::checksumHeader() const
|
||||
{
|
||||
return d->_checksumHeader;
|
||||
}
|
||||
|
||||
time_t SyncJournalFileRecord::modtime() const
|
||||
{
|
||||
return d->_modtime.value_or(0);
|
||||
}
|
||||
|
||||
int64_t SyncJournalFileRecord::size() const
|
||||
{
|
||||
return d->_fileSize;
|
||||
}
|
||||
|
||||
uint64_t SyncJournalFileRecord::inode() const
|
||||
{
|
||||
return d->_inode;
|
||||
}
|
||||
|
||||
ItemType SyncJournalFileRecord::type() const
|
||||
{
|
||||
return d->_type;
|
||||
}
|
||||
|
||||
bool SyncJournalFileRecord::hasDirtyPlaceholder() const
|
||||
{
|
||||
return d->_hasDirtyPlaceholder;
|
||||
}
|
||||
|
||||
void SyncJournalFileRecord::setDirtyPlaceholder(bool b)
|
||||
{
|
||||
d.detach();
|
||||
d->_hasDirtyPlaceholder = b;
|
||||
}
|
||||
|
||||
bool SyncJournalFileRecord::serverHasIgnoredFiles() const
|
||||
{
|
||||
return d->_serverHasIgnoredFiles;
|
||||
}
|
||||
|
||||
QString SyncJournalFileRecord::error() const
|
||||
{
|
||||
return d->_error;
|
||||
}
|
||||
|
||||
bool SyncJournalFileRecord::hasError() const
|
||||
{
|
||||
return !d->_error.isEmpty();
|
||||
}
|
||||
QDebug OCC::operator<<(QDebug debug, const SyncJournalFileRecord &record)
|
||||
{
|
||||
QDebugStateSaver saver(debug);
|
||||
debug.nospace() << u"SyncJournalFileRecord(";
|
||||
if (record.hasError()) {
|
||||
debug << u"Error: " << record.error();
|
||||
} else {
|
||||
debug << u"path: " << record.path() << u", inode: " << record.inode() << u", modtime: " << record.modtime() << u", type: " << record.type()
|
||||
<< u", etag: " << record.etag() << u", fileId: " << record.fileId() << u", remotePerm: " << record.remotePerm().toString() << u", size: "
|
||||
<< record.size() << u", checksum: " << record.checksumHeader() << u", serverHasIgnoredFiles: " << record.serverHasIgnoredFiles()
|
||||
<< u", hasDirtyPlaceholder: " << record.hasDirtyPlaceholder();
|
||||
}
|
||||
return debug << u")";
|
||||
}
|
||||
|
||||
bool operator==(const SyncJournalFileRecord &lhs, const SyncJournalFileRecord &rhs)
|
||||
{
|
||||
return lhs.path() == rhs.path() && lhs.inode() == rhs.inode() && lhs.modtime() == rhs.modtime() && lhs.type() == rhs.type() && lhs.etag() == rhs.etag()
|
||||
&& lhs.fileId() == rhs.fileId() && lhs.size() == rhs.size() && lhs.remotePerm() == rhs.remotePerm()
|
||||
&& lhs.serverHasIgnoredFiles() == rhs.serverHasIgnoredFiles() && lhs.checksumHeader() == rhs.checksumHeader();
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef SYNCJOURNALFILERECORD_H
|
||||
#define SYNCJOURNALFILERECORD_H
|
||||
|
||||
#include "libsync/common/utility.h"
|
||||
#include "libsync/csync.h"
|
||||
#include "libsync/qsferasynclib.h"
|
||||
#include "remotepermissions.h"
|
||||
|
||||
namespace OCC {
|
||||
class SqlQuery;
|
||||
|
||||
class SyncFileItem;
|
||||
class SyncJournalFileRecordData;
|
||||
|
||||
/**
|
||||
* @brief The SyncJournalFileRecord class
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT SyncJournalFileRecord
|
||||
{
|
||||
public:
|
||||
SyncJournalFileRecord();
|
||||
SyncJournalFileRecord(const QString &error);
|
||||
~SyncJournalFileRecord();
|
||||
SyncJournalFileRecord(const SyncJournalFileRecord &other);
|
||||
SyncJournalFileRecord &operator=(const SyncJournalFileRecord &other);
|
||||
void swap(SyncJournalFileRecord &other) noexcept { d.swap(other.d); }
|
||||
QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP(SyncJournalFileRecord);
|
||||
|
||||
static QByteArray query();
|
||||
static SyncJournalFileRecord fromSqlQuery(SqlQuery &query);
|
||||
static SyncJournalFileRecord fromSyncFileItem(const SyncFileItem &syncFile);
|
||||
bool isValid() const;
|
||||
|
||||
QDateTime modDateTime() const;
|
||||
|
||||
bool isDirectory() const;
|
||||
bool isFile() const;
|
||||
bool isVirtualFile() const;
|
||||
|
||||
QString path() const;
|
||||
QString name() const;
|
||||
QString etag() const;
|
||||
QByteArray fileId() const;
|
||||
RemotePermissions remotePerm() const;
|
||||
QByteArray checksumHeader() const;
|
||||
time_t modtime() const;
|
||||
int64_t size() const;
|
||||
uint64_t inode() const;
|
||||
ItemType type() const;
|
||||
|
||||
bool hasDirtyPlaceholder() const;
|
||||
void setDirtyPlaceholder(bool b);
|
||||
bool serverHasIgnoredFiles() const;
|
||||
|
||||
QString error() const;
|
||||
bool hasError() const;
|
||||
|
||||
private:
|
||||
// constructor for the non null object
|
||||
SyncJournalFileRecord(const QString &path, ItemType type);
|
||||
bool validateRecord();
|
||||
|
||||
QExplicitlySharedDataPointer<SyncJournalFileRecordData> d;
|
||||
};
|
||||
|
||||
QDebug QSFERA_SYNC_EXPORT operator<<(QDebug debug, const SyncJournalFileRecord &record);
|
||||
|
||||
class QSFERA_SYNC_EXPORT SyncJournalErrorBlacklistRecord
|
||||
{
|
||||
Q_GADGET
|
||||
public:
|
||||
enum class Category {
|
||||
/// Normal errors have no special behavior
|
||||
Normal = 0,
|
||||
/// These get a special summary message
|
||||
InsufficientRemoteStorage,
|
||||
LocalSoftError
|
||||
};
|
||||
Q_ENUM(Category)
|
||||
|
||||
SyncJournalErrorBlacklistRecord()
|
||||
: _retryCount(0)
|
||||
, _errorCategory(Category::Normal)
|
||||
, _lastTryModtime(0)
|
||||
, _lastTryTime(0)
|
||||
, _ignoreDuration(0)
|
||||
{
|
||||
}
|
||||
|
||||
/// The number of times the operation was unsuccessful so far.
|
||||
int _retryCount;
|
||||
|
||||
/// The last error string.
|
||||
QString _errorString;
|
||||
/// The error category. Sometimes used for special actions.
|
||||
Category _errorCategory;
|
||||
|
||||
qint64 _lastTryModtime;
|
||||
QByteArray _lastTryEtag;
|
||||
|
||||
/// The last time the operation was attempted (in s since epoch).
|
||||
qint64 _lastTryTime;
|
||||
|
||||
/// The number of seconds the file shall be ignored.
|
||||
qint64 _ignoreDuration;
|
||||
|
||||
QString _file;
|
||||
QString _renameTarget;
|
||||
|
||||
/// The last X-Request-ID of the request that failled
|
||||
QByteArray _requestId;
|
||||
|
||||
bool isValid() const;
|
||||
};
|
||||
|
||||
/** Represents a conflict in the conflicts table.
|
||||
*
|
||||
* In the following the "conflict file" is the file that has the conflict
|
||||
* tag in the filename, and the base file is the file that it's a conflict for.
|
||||
* So if "a/foo.txt" is the base file, its conflict file could be
|
||||
* "a/foo (conflicted copy 1234).txt".
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT ConflictRecord
|
||||
{
|
||||
public:
|
||||
/** Path to the file with the conflict tag in the name
|
||||
*
|
||||
* The path is sync-folder relative.
|
||||
*/
|
||||
QByteArray path;
|
||||
|
||||
/// File id of the base file
|
||||
QByteArray baseFileId;
|
||||
|
||||
/** Modtime of the base file
|
||||
*
|
||||
* may not be available and be -1
|
||||
*/
|
||||
qint64 baseModtime = -1;
|
||||
|
||||
/** Etag of the base file
|
||||
*
|
||||
* may not be available and empty
|
||||
*/
|
||||
QByteArray baseEtag;
|
||||
|
||||
/**
|
||||
* The path of the original file at the time the conflict was created
|
||||
*
|
||||
* Note that in nearly all cases one should query the db by baseFileId and
|
||||
* thus retrieve the *current* base path instead!
|
||||
*
|
||||
* maybe be empty if not available
|
||||
*/
|
||||
QByteArray initialBasePath;
|
||||
|
||||
|
||||
bool isValid() const { return !path.isEmpty(); }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
QSFERA_SYNC_EXPORT bool operator==(const OCC::SyncJournalFileRecord &, const OCC::SyncJournalFileRecord &);
|
||||
Q_DECLARE_SHARED(OCC::SyncJournalFileRecord);
|
||||
#endif // SYNCJOURNALFILERECORD_H
|
||||
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
* Copyright (C) by Daniel Molkentin <danimo@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 "common/utility.h"
|
||||
#include "common/asserts.h"
|
||||
#include "common/filesystembase.h"
|
||||
#include "common/version.h"
|
||||
|
||||
// Note: This file must compile without QtGui
|
||||
#include <QCollator>
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QSysInfo>
|
||||
#include <QTextStream>
|
||||
#include <QThread>
|
||||
#include <QTimeZone>
|
||||
#include <QUrl>
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <sys/statvfs.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#ifdef Q_OS_WIN
|
||||
#include <qt_windows.h>
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
#include <math.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
using namespace std::chrono;
|
||||
namespace {
|
||||
auto RFC1123PatternC()
|
||||
{
|
||||
return QStringLiteral("ddd, dd MMM yyyy HH:mm:ss 'GMT'");
|
||||
}
|
||||
}
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcUtility, "sync.utility", QtMsgType::QtInfoMsg)
|
||||
|
||||
QString Utility::octetsToString(qint64 octets)
|
||||
{
|
||||
OC_ASSERT(octets >= 0)
|
||||
|
||||
using namespace FileSystem::SizeLiterals;
|
||||
|
||||
// We do what macOS 10.8 and above do: 0 fraction digits for bytes and KB; 1 fraction digits for MB; 2 for GB and above.
|
||||
// See also https://developer.apple.com/documentation/foundation/nsbytecountformatter/1417887-adaptive
|
||||
int precision = 0;
|
||||
if (quint64(octets) >= 1_GiB) {
|
||||
precision = 2;
|
||||
} else if (quint64(octets) >= 1_MiB) {
|
||||
precision = 1;
|
||||
}
|
||||
|
||||
return QLocale().formattedDataSize(octets, precision, QLocale::DataSizeTraditionalFormat);
|
||||
}
|
||||
|
||||
static QLatin1String platform()
|
||||
{
|
||||
#if defined(Q_OS_WIN)
|
||||
return QLatin1String("Windows");
|
||||
#elif defined(Q_OS_MAC)
|
||||
return QLatin1String("Macintosh");
|
||||
#elif defined(Q_OS_LINUX)
|
||||
return QLatin1String("Linux");
|
||||
#elif defined(__DragonFly__) // Q_OS_FREEBSD also defined
|
||||
return QLatin1String("DragonFlyBSD");
|
||||
#elif defined(Q_OS_FREEBSD) || defined(Q_OS_FREEBSD_KERNEL)
|
||||
return QLatin1String("FreeBSD");
|
||||
#elif defined(Q_OS_NETBSD)
|
||||
return QLatin1String("NetBSD");
|
||||
#elif defined(Q_OS_OPENBSD)
|
||||
return QLatin1String("OpenBSD");
|
||||
#elif defined(Q_OS_SOLARIS)
|
||||
return QLatin1String("Solaris");
|
||||
#else
|
||||
return QSysInfo::productType();
|
||||
#endif
|
||||
}
|
||||
|
||||
QByteArray Utility::userAgentString()
|
||||
{
|
||||
return QStringLiteral("Mozilla/5.0 (%1) mirall/%2 (%3, %4-%5 ClientArchitecture: %6 OsArchitecture: %7)")
|
||||
.arg(platform(), OCC::Version::displayString(),
|
||||
// accessing the theme to fetch the string is rather difficult
|
||||
// since this is only needed server-side to identify clients, the app name (as of 2.9, the short name) is good enough
|
||||
qApp->applicationName(), QSysInfo::productType(), QSysInfo::kernelVersion(), QSysInfo::buildCpuArchitecture(), Utility::currentCpuArch())
|
||||
.toLatin1();
|
||||
}
|
||||
|
||||
qint64 Utility::freeDiskSpace(const QString &path)
|
||||
{
|
||||
#if defined(Q_OS_MAC) || defined(Q_OS_FREEBSD) || defined(Q_OS_FREEBSD_KERNEL) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD)
|
||||
struct statvfs stat;
|
||||
if (statvfs(path.toLocal8Bit().data(), &stat) == 0) {
|
||||
return (qint64)stat.f_bavail * stat.f_frsize;
|
||||
}
|
||||
#elif defined(Q_OS_UNIX)
|
||||
struct statvfs64 stat;
|
||||
if (statvfs64(path.toLocal8Bit().data(), &stat) == 0) {
|
||||
return (qint64)stat.f_bavail * stat.f_frsize;
|
||||
}
|
||||
#elif defined(Q_OS_WIN)
|
||||
ULARGE_INTEGER freeBytes;
|
||||
freeBytes.QuadPart = 0L;
|
||||
if (GetDiskFreeSpaceEx(reinterpret_cast<const wchar_t *>(FileSystem::longWinPath(path).utf16()), &freeBytes, nullptr, nullptr)) {
|
||||
return freeBytes.QuadPart;
|
||||
}
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString Utility::compactFormatDouble(double value, int prec, const QString &unit)
|
||||
{
|
||||
QLocale locale = QLocale::system();
|
||||
const QString decPoint = locale.decimalPoint();
|
||||
QString str = locale.toString(value, 'f', prec);
|
||||
while (str.endsWith(QLatin1Char('0')) || str.endsWith(decPoint)) {
|
||||
if (str.endsWith(decPoint)) {
|
||||
str.chop(1);
|
||||
break;
|
||||
}
|
||||
str.chop(1);
|
||||
}
|
||||
if (!unit.isEmpty())
|
||||
str += (QLatin1Char(' ') + unit);
|
||||
return str;
|
||||
}
|
||||
|
||||
QString Utility::escape(const QString &in)
|
||||
{
|
||||
return in.toHtmlEscaped();
|
||||
}
|
||||
|
||||
// This can be overridden from the tests
|
||||
QSFERA_SYNC_EXPORT bool fsCasePreserving_override = Utility::isWindows() || Utility::isMac();
|
||||
|
||||
bool Utility::fsCasePreserving()
|
||||
{
|
||||
return fsCasePreserving_override;
|
||||
}
|
||||
|
||||
bool Utility::fileNamesEqual(const QString &fn1, const QString &fn2)
|
||||
{
|
||||
const QDir fd1(fn1);
|
||||
const QDir fd2(fn2);
|
||||
|
||||
// Attention: If the path does not exist, canonicalPath returns ""
|
||||
// ONLY use this function with existing pathes.
|
||||
const QString a = fd1.canonicalPath();
|
||||
const QString b = fd2.canonicalPath();
|
||||
bool re = !a.isEmpty() && QString::compare(a, b, fsCasePreserving() ? Qt::CaseInsensitive : Qt::CaseSensitive) == 0;
|
||||
return re;
|
||||
}
|
||||
|
||||
QDateTime Utility::qDateTimeFromTime_t(qint64 t)
|
||||
{
|
||||
return QDateTime::fromMSecsSinceEpoch(t * 1000);
|
||||
}
|
||||
|
||||
qint64 Utility::qDateTimeToTime_t(const QDateTime &t)
|
||||
{
|
||||
return t.toMSecsSinceEpoch() / 1000;
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr struct
|
||||
{
|
||||
const char *const name;
|
||||
const std::chrono::milliseconds msec;
|
||||
QString description(int value) const { return QCoreApplication::translate("Utility", name, nullptr, value); }
|
||||
} periods[] = {
|
||||
{QT_TRANSLATE_N_NOOP("Utility", "%n year(s)"), 24h * 365},
|
||||
{QT_TRANSLATE_N_NOOP("Utility", "%n month(s)"), 24h * 30},
|
||||
{QT_TRANSLATE_N_NOOP("Utility", "%n day(s)"), 24h},
|
||||
{QT_TRANSLATE_N_NOOP("Utility", "%n hour(s)"), 1h},
|
||||
{QT_TRANSLATE_N_NOOP("Utility", "%n minute(s)"), 1min},
|
||||
{QT_TRANSLATE_N_NOOP("Utility", "%n second(s)"), 1s},
|
||||
{nullptr, {}},
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
QString Utility::durationToDescriptiveString2(std::chrono::milliseconds msecs)
|
||||
{
|
||||
int p = 0;
|
||||
while (periods[p + 1].name && msecs < periods[p].msec) {
|
||||
p++;
|
||||
}
|
||||
|
||||
const QString firstPart = periods[p].description(static_cast<int>(msecs / periods[p].msec));
|
||||
|
||||
if (!periods[p + 1].name) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
const quint64 secondPartNum = qRound(static_cast<double>(msecs.count() % periods[p].msec.count()) / periods[p + 1].msec.count());
|
||||
|
||||
if (secondPartNum == 0) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
return QCoreApplication::translate("Utility", "%1 %2").arg(firstPart, periods[p + 1].description(secondPartNum));
|
||||
}
|
||||
|
||||
QString Utility::durationToDescriptiveString1(std::chrono::milliseconds msecs)
|
||||
{
|
||||
int p = 0;
|
||||
while (periods[p + 1].name && msecs < periods[p].msec) {
|
||||
p++;
|
||||
}
|
||||
return periods[p].description(qRound(static_cast<double>(msecs.count()) / periods[p].msec.count()));
|
||||
}
|
||||
|
||||
QString Utility::fileNameForGuiUse(const QString &fName)
|
||||
{
|
||||
if (isMac()) {
|
||||
QString n(fName);
|
||||
return n.replace(QLatin1Char(':'), QLatin1Char('/'));
|
||||
}
|
||||
return fName;
|
||||
}
|
||||
|
||||
QString Utility::normalizeEtag(QStringView etag)
|
||||
{
|
||||
if (etag.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto unQuote = [&] {
|
||||
if (etag.startsWith(QLatin1Char('"')) && etag.endsWith(QLatin1Char('"'))) {
|
||||
etag = etag.mid(1);
|
||||
etag.chop(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Weak E-Tags can appear when gzip compression is on, see #3946
|
||||
if (etag.startsWith(QLatin1String("W/"))) {
|
||||
etag = etag.mid(2);
|
||||
}
|
||||
/* strip normal quotes and quotes around "XXXX-gzip" */
|
||||
unQuote();
|
||||
|
||||
// https://github.com/owncloud/client/issues/1195
|
||||
const QLatin1String gzipSuffix("-gzip");
|
||||
if (etag.endsWith(gzipSuffix)) {
|
||||
etag.chop(gzipSuffix.size());
|
||||
}
|
||||
/* strip normal quotes */
|
||||
unQuote();
|
||||
return etag.toString();
|
||||
}
|
||||
|
||||
QString Utility::platformName()
|
||||
{
|
||||
return QSysInfo::prettyProductName();
|
||||
}
|
||||
|
||||
void Utility::crash()
|
||||
{
|
||||
volatile int *a = (int *)nullptr;
|
||||
*a = 1;
|
||||
}
|
||||
|
||||
QString Utility::timeAgoInWords(const QDateTime &dt, const QDateTime &from)
|
||||
{
|
||||
const QDateTime now = from.isValid() ? from : QDateTime::currentDateTimeUtc();
|
||||
|
||||
if (dt.daysTo(now) > 0) {
|
||||
return QObject::tr("%n day(s) ago", "", dt.daysTo(now));
|
||||
}
|
||||
|
||||
const qint64 secs = dt.secsTo(now);
|
||||
if (secs < 0) {
|
||||
return QObject::tr("in the future");
|
||||
}
|
||||
|
||||
if (floor(secs / 3600.0) > 0) {
|
||||
const int hours = floor(secs / 3600.0);
|
||||
return (QObject::tr("%n hour(s) ago", "", hours));
|
||||
}
|
||||
|
||||
const int minutes = qRound(secs / 60.0);
|
||||
if (minutes == 0) {
|
||||
if (secs < 5) {
|
||||
return QObject::tr("now");
|
||||
} else {
|
||||
return QObject::tr("less than a minute ago");
|
||||
}
|
||||
}
|
||||
|
||||
return (QObject::tr("%n minute(s) ago", "", minutes));
|
||||
}
|
||||
|
||||
void Utility::sortFilenames(QStringList &fileNames)
|
||||
{
|
||||
QCollator collator;
|
||||
collator.setNumericMode(true);
|
||||
collator.setCaseSensitivity(Qt::CaseInsensitive);
|
||||
std::sort(fileNames.begin(), fileNames.end(), collator);
|
||||
}
|
||||
|
||||
QString Utility::concatUrlPathItems(QStringList &&items, const QLatin1Char delimiter)
|
||||
{
|
||||
for (QString &item : items) {
|
||||
while (item.endsWith(QLatin1Char('/'))) {
|
||||
item.chop(1);
|
||||
}
|
||||
}
|
||||
return items.join(delimiter);
|
||||
}
|
||||
|
||||
QUrl Utility::concatUrlPath(const QUrl &url, const QString &concatPath, const QUrlQuery &queryItems)
|
||||
{
|
||||
QString path = url.path();
|
||||
if (!concatPath.isEmpty()) {
|
||||
// avoid '//'
|
||||
if (path.endsWith(QLatin1Char('/')) && concatPath.startsWith(QLatin1Char('/'))) {
|
||||
path.chop(1);
|
||||
} // avoid missing '/'
|
||||
else if (!path.endsWith(QLatin1Char('/')) && !concatPath.startsWith(QLatin1Char('/'))) {
|
||||
path += QLatin1Char('/');
|
||||
}
|
||||
path += concatPath; // put the complete path together
|
||||
}
|
||||
Q_ASSERT(!path.contains(QStringLiteral("//")));
|
||||
Q_ASSERT(url.query().isEmpty());
|
||||
QUrl tmpUrl = url;
|
||||
tmpUrl.setPath(path);
|
||||
tmpUrl.setQuery(queryItems);
|
||||
return tmpUrl;
|
||||
}
|
||||
|
||||
bool Utility::urlEqual(QUrl url1, QUrl url2)
|
||||
{
|
||||
// ensure https://demo.opencloud.eu/ matches https://demo.opencloud.eu
|
||||
// the empty path was the legacy formating before 2.9
|
||||
if (url1.path().isEmpty()) {
|
||||
url1.setPath(QStringLiteral("/"));
|
||||
}
|
||||
if (url2.path().isEmpty()) {
|
||||
url2.setPath(QStringLiteral("/"));
|
||||
}
|
||||
|
||||
return url1.matches(url2, QUrl::StripTrailingSlash | QUrl::NormalizePathSegments);
|
||||
}
|
||||
|
||||
QString Utility::makeConflictFileName(const QString &fn, const QDateTime &dt, const QString &user)
|
||||
{
|
||||
QString conflictFileName(fn);
|
||||
// Add conflict tag before the extension.
|
||||
int dotLocation = conflictFileName.lastIndexOf(QLatin1Char('.'));
|
||||
// If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file)
|
||||
if (dotLocation <= conflictFileName.lastIndexOf(QLatin1Char('/')) + 1) {
|
||||
dotLocation = conflictFileName.size();
|
||||
}
|
||||
|
||||
QString conflictMarker = QStringLiteral(" (conflicted copy ");
|
||||
if (!user.isEmpty()) {
|
||||
// Don't allow parens in the user name, to ensure
|
||||
// we can find the beginning and end of the conflict tag.
|
||||
const auto userName = sanitizeForFileName(user).replace(QLatin1Char('('), QLatin1Char('_')).replace(QLatin1Char(')'), QLatin1Char('_'));
|
||||
;
|
||||
conflictMarker += userName + QLatin1Char(' ');
|
||||
}
|
||||
conflictMarker += dt.toString(QStringLiteral("yyyy-MM-dd hhmmss")) + QLatin1Char(')');
|
||||
|
||||
conflictFileName.insert(dotLocation, conflictMarker);
|
||||
return conflictFileName;
|
||||
}
|
||||
|
||||
bool Utility::isConflictFile(QStringView name)
|
||||
{
|
||||
auto bname = name.mid(name.lastIndexOf(QLatin1Char('/')) + 1);
|
||||
|
||||
if (bname.contains(QStringLiteral("_conflict-")))
|
||||
return true;
|
||||
|
||||
if (bname.contains(QStringLiteral("(conflicted copy")))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString Utility::conflictFileBaseNameFromPattern(const QString &conflictName)
|
||||
{
|
||||
// This function must be able to deal with conflict files for conflict files.
|
||||
// To do this, we scan backwards, for the outermost conflict marker and
|
||||
// strip only that to generate the conflict file base name.
|
||||
auto startOld = conflictName.lastIndexOf("_conflict-"_L1);
|
||||
|
||||
// A single space before "(conflicted copy" is considered part of the tag
|
||||
auto startNew = conflictName.lastIndexOf("(conflicted copy"_L1);
|
||||
if (startNew > 0 && conflictName[startNew - 1] == ' '_L1)
|
||||
startNew -= 1;
|
||||
|
||||
// The rightmost tag is relevant
|
||||
auto tagStart = qMax(startOld, startNew);
|
||||
if (tagStart == -1)
|
||||
return QString();
|
||||
|
||||
// Find the end of the tag
|
||||
auto tagEnd = conflictName.size();
|
||||
auto dot = conflictName.lastIndexOf('.'_L1); // dot could be part of user name for new tag!
|
||||
if (dot > tagStart)
|
||||
tagEnd = dot;
|
||||
if (tagStart == startNew) {
|
||||
auto paren = conflictName.indexOf(')'_L1, tagStart);
|
||||
if (paren != -1)
|
||||
tagEnd = paren + 1;
|
||||
}
|
||||
return conflictName.left(tagStart) + conflictName.mid(tagEnd);
|
||||
}
|
||||
|
||||
QString Utility::sanitizeForFileName(const QString &name)
|
||||
{
|
||||
const auto invalid = QStringLiteral("/?<>\\:*|\"");
|
||||
QString result;
|
||||
result.reserve(name.size());
|
||||
for (const auto &c : name) {
|
||||
if (!invalid.contains(c) && c.category() != QChar::Other_Control && c.category() != QChar::Other_Format) {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
QString Utility::appImageLocation()
|
||||
{
|
||||
Q_ASSERT(Utility::runningInAppImage());
|
||||
static const auto value = qEnvironmentVariable("APPIMAGE");
|
||||
return value;
|
||||
}
|
||||
|
||||
bool Utility::runningInAppImage()
|
||||
{
|
||||
return qEnvironmentVariableIsSet("APPIMAGE");
|
||||
}
|
||||
#endif
|
||||
|
||||
QDateTime Utility::parseRFC1123Date(const QString &date)
|
||||
{
|
||||
if (!date.isEmpty()) {
|
||||
auto out = QDateTime::fromString(date, RFC1123PatternC());
|
||||
Q_ASSERT(out.isValid());
|
||||
out.setTimeZone(QTimeZone::utc());
|
||||
return out;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QString Utility::formatRFC1123Date(const QDateTime &date)
|
||||
{
|
||||
return date.toUTC().toString(RFC1123PatternC());
|
||||
}
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
QString Utility::currentCpuArch()
|
||||
{
|
||||
return QSysInfo::currentCpuArchitecture();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
QDebug operator<<(QDebug debug, nanoseconds in)
|
||||
{
|
||||
QDebugStateSaver save(debug);
|
||||
debug.nospace();
|
||||
const auto h = duration_cast<hours>(in);
|
||||
const auto min = duration_cast<minutes>(in -= h);
|
||||
const auto s = duration_cast<seconds>(in -= min);
|
||||
const auto ms = duration_cast<milliseconds>(in -= s);
|
||||
return debug << u"duration(" << h.count() << u"h, " << min.count() << u"min, " << s.count() << u"s, " << ms.count() << u"ms)";
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
* Copyright (C) by Daniel Molkentin <danimo@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
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDateTime>
|
||||
#include <QElapsedTimer>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMetaEnum>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
class QSettings;
|
||||
class QQuickWidget;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class SyncJournal;
|
||||
|
||||
QSFERA_SYNC_EXPORT Q_DECLARE_LOGGING_CATEGORY(lcUtility)
|
||||
|
||||
/** \addtogroup libsync
|
||||
* @{
|
||||
*/
|
||||
namespace Utility
|
||||
{
|
||||
QSFERA_SYNC_EXPORT void setupFavLink(const QString &folder);
|
||||
QSFERA_SYNC_EXPORT QString octetsToString(qint64 octets);
|
||||
QSFERA_SYNC_EXPORT QByteArray userAgentString();
|
||||
|
||||
/**
|
||||
* @brief Return whether launch on startup is enabled system wide.
|
||||
*
|
||||
* If this returns true, the checkbox for user specific launch
|
||||
* on startup will be hidden.
|
||||
*
|
||||
* Currently only implemented on Windows.
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT bool hasSystemLaunchOnStartup(const QString &appName);
|
||||
|
||||
/**
|
||||
* @brief Return if launch on startup is enabled for the current user.
|
||||
* @param appName the name of the application (because of branding)
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT bool hasLaunchOnStartup(const QString &appName);
|
||||
QSFERA_SYNC_EXPORT void setLaunchOnStartup(const QString &appName, const QString &guiName, bool launch);
|
||||
|
||||
/**
|
||||
* Return the amount of free space available.
|
||||
*
|
||||
* \a path must point to a directory
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT qint64 freeDiskSpace(const QString &path);
|
||||
|
||||
/**
|
||||
* @brief compactFormatDouble - formats a double value human readable.
|
||||
*
|
||||
* @param value the value to format.
|
||||
* @param prec the precision.
|
||||
* @param unit an optional unit that is appended if present.
|
||||
* @return the formatted string.
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT QString compactFormatDouble(double value, int prec, const QString &unit = QString());
|
||||
|
||||
// porting methods
|
||||
QSFERA_SYNC_EXPORT QString escape(const QString &);
|
||||
|
||||
// conversion function QDateTime <-> time_t (because the ones builtin work on only unsigned 32bit)
|
||||
QSFERA_SYNC_EXPORT QDateTime qDateTimeFromTime_t(qint64 t);
|
||||
QSFERA_SYNC_EXPORT qint64 qDateTimeToTime_t(const QDateTime &t);
|
||||
|
||||
/**
|
||||
* @brief Convert milliseconds duration to human readable string.
|
||||
* @param msecs the milliseconds to convert to string.
|
||||
* @return an HMS representation of the milliseconds value.
|
||||
*
|
||||
* durationToDescriptiveString1 describes the duration in a single
|
||||
* unit, like "5 minutes" or "2 days".
|
||||
*
|
||||
* durationToDescriptiveString2 uses two units where possible, so
|
||||
* "5 minutes 43 seconds" or "1 month 3 days".
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT QString durationToDescriptiveString1(std::chrono::milliseconds msecs);
|
||||
QSFERA_SYNC_EXPORT QString durationToDescriptiveString2(std::chrono::milliseconds msecs);
|
||||
|
||||
/**
|
||||
* @brief hasDarkSystray - determines whether the systray is dark or light.
|
||||
*
|
||||
* Use this to check if the OS has a dark or a light systray.
|
||||
*
|
||||
* The value might change during the execution of the program
|
||||
* (e.g. on OS X 10.10).
|
||||
*
|
||||
* @return bool which is true for systems with dark systray.
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT bool hasDarkSystray();
|
||||
|
||||
// convenience OS detection methods
|
||||
constexpr bool isWindows();
|
||||
constexpr bool isMac();
|
||||
constexpr bool isUnix();
|
||||
constexpr bool isLinux(); // use with care
|
||||
constexpr bool isBSD(); // use with care, does not match OS X
|
||||
|
||||
QSFERA_SYNC_EXPORT QString platformName();
|
||||
// crash helper for --debug
|
||||
QSFERA_SYNC_EXPORT void crash();
|
||||
|
||||
// Case preserving file system underneath?
|
||||
// if this function returns true, the file system is case preserving,
|
||||
// that means "test" means the same as "TEST" for filenames.
|
||||
// if false, the two cases are two different files.
|
||||
QSFERA_SYNC_EXPORT bool fsCasePreserving();
|
||||
inline auto fsCaseSensitivity()
|
||||
{
|
||||
return fsCasePreserving() ? Qt::CaseInsensitive : Qt::CaseSensitive;
|
||||
}
|
||||
|
||||
// Check if two pathes that MUST exist are equal. This function
|
||||
// uses QDir::canonicalPath() to judge and cares for the systems
|
||||
// case sensitivity.
|
||||
QSFERA_SYNC_EXPORT bool fileNamesEqual(const QString &fn1, const QString &fn2);
|
||||
|
||||
inline auto stripTrailingSlash(QStringView s)
|
||||
{
|
||||
if (s.endsWith(QLatin1Char('/'))) {
|
||||
s.chop(1);
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
inline auto stripLeadingSlash(QStringView s)
|
||||
{
|
||||
if (s.startsWith(QLatin1Char('/'))) {
|
||||
s.slice(1);
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
inline QString ensureTrailingSlash(QStringView s)
|
||||
{
|
||||
if (!s.endsWith(QLatin1Char('/'))) {
|
||||
return s.toString() + QLatin1Char('/');
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
// Call the given command with the switch --version and rerun the first line
|
||||
// of the output.
|
||||
// If command is empty, the function calls the running application which, on
|
||||
// Linux, might have changed while this one is running.
|
||||
// For Mac and Windows, it returns QString()
|
||||
QSFERA_SYNC_EXPORT QString versionOfInstalledBinary(const QString &command = QString());
|
||||
|
||||
QSFERA_SYNC_EXPORT QString fileNameForGuiUse(const QString &fName);
|
||||
|
||||
QSFERA_SYNC_EXPORT QString normalizeEtag(QStringView etag);
|
||||
|
||||
/**
|
||||
* @brief timeAgoInWords - human readable time span
|
||||
*
|
||||
* Use this to get a string that describes the timespan between the first and
|
||||
* the second timestamp in a human readable and understandable form.
|
||||
*
|
||||
* If the second parameter is ommitted, the current time is used.
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT QString timeAgoInWords(const QDateTime &dt, const QDateTime &from = QDateTime());
|
||||
|
||||
/**
|
||||
* @brief Sort a QStringList in a way that's appropriate for filenames
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT void sortFilenames(QStringList & fileNames);
|
||||
|
||||
/**
|
||||
* Concatenate parts of a URL path with a given delimiter, eliminating duplicate
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT QString concatUrlPathItems(QStringList && items, const QLatin1Char delimiter = QLatin1Char('/'));
|
||||
|
||||
/** Appends concatPath and queryItems to the url */
|
||||
QSFERA_SYNC_EXPORT QUrl concatUrlPath(const QUrl &url, const QString &concatPath, const QUrlQuery &queryItems = {});
|
||||
|
||||
/** Compares two urls and ignores whether thei end wit / */
|
||||
QSFERA_SYNC_EXPORT bool urlEqual(QUrl a, QUrl b);
|
||||
|
||||
/** Returns a new settings pre-set in a specific group. The Settings will be created
|
||||
with the given parent. If no parent is specified, the caller must destroy the settings */
|
||||
QSFERA_SYNC_EXPORT std::unique_ptr<QSettings> settingsWithGroup(const QString &group, QObject *parent = nullptr);
|
||||
|
||||
/** Sanitizes a string that shall become part of a filename.
|
||||
*
|
||||
* Filters out reserved characters like
|
||||
* - unicode control and format characters
|
||||
* - reserved characters: /, ?, <, >, \, :, *, |, and "
|
||||
*
|
||||
* Warning: This does not sanitize the whole resulting string, so
|
||||
* - unix reserved filenames ('.', '..')
|
||||
* - trailing periods and spaces
|
||||
* - windows reserved filenames ('CON' etc)
|
||||
* will pass unchanged.
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT QString sanitizeForFileName(const QString &name);
|
||||
|
||||
/** Returns a file name based on \a fn that's suitable for a conflict.
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT QString makeConflictFileName(const QString &fn, const QDateTime &dt, const QString &user);
|
||||
|
||||
/** Returns whether a file name indicates a conflict file
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT bool isConflictFile(QStringView name);
|
||||
|
||||
/** Find the base name for a conflict file name, using name pattern only
|
||||
*
|
||||
* Will return an empty string if it's not a conflict file.
|
||||
*
|
||||
* Prefer to use the data from the conflicts table in the journal to determine
|
||||
* a conflict's base file, see SyncJournal::conflictFileBaseName()
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT QString conflictFileBaseNameFromPattern(const QString &conflictName);
|
||||
|
||||
template <class E>
|
||||
E stringToEnum(const char *key)
|
||||
{
|
||||
bool ok;
|
||||
auto out = static_cast<E>(QMetaEnum::fromType<E>().keyToValue(key, &ok));
|
||||
Q_ASSERT(ok);
|
||||
return out;
|
||||
}
|
||||
|
||||
template <class E>
|
||||
E stringToEnum(const QString &key)
|
||||
{
|
||||
return stringToEnum<E>(key.toUtf8().constData());
|
||||
}
|
||||
|
||||
template <class E>
|
||||
QString enumToString(E value)
|
||||
{
|
||||
return QString::fromUtf8(QMetaEnum::fromType<E>().valueToKeys(static_cast<int>(value)));
|
||||
}
|
||||
|
||||
template <class E = void>
|
||||
QString enumToDisplayName(E)
|
||||
{
|
||||
static_assert(std::is_same<E, void>::value, "Not implemented");
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
class asKeyValueRange
|
||||
{
|
||||
// https://www.kdab.com/qt-range-based-for-loops-and-structured-bindings/
|
||||
public:
|
||||
asKeyValueRange(const T &data)
|
||||
: _data{data}
|
||||
{
|
||||
}
|
||||
|
||||
auto begin() { return _data.constKeyValueBegin(); }
|
||||
|
||||
auto end() { return _data.constKeyValueEnd(); }
|
||||
|
||||
private:
|
||||
const T &_data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform a const find on a Qt container and returns an std::optional<const_iterator>
|
||||
* This allows performant access to the container with in a simple if condition.
|
||||
* if (auto it = optionalFind("key"))
|
||||
*/
|
||||
template <typename T, typename K>
|
||||
auto optionalFind(const T &container, const K &key)
|
||||
{
|
||||
auto it = container.constFind(key);
|
||||
return it == container.cend() ? std::nullopt : std::make_optional(it);
|
||||
}
|
||||
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
QSFERA_SYNC_EXPORT QString appImageLocation();
|
||||
QSFERA_SYNC_EXPORT bool runningInAppImage();
|
||||
#else
|
||||
inline QString appImageLocation()
|
||||
{
|
||||
Q_UNREACHABLE();
|
||||
};
|
||||
constexpr bool runningInAppImage()
|
||||
{
|
||||
return false;
|
||||
};
|
||||
#endif
|
||||
|
||||
QSFERA_SYNC_EXPORT QDateTime parseRFC1123Date(const QString &date);
|
||||
QSFERA_SYNC_EXPORT QString formatRFC1123Date(const QDateTime &date);
|
||||
|
||||
// replacement for QSysInfo::currentCpuArchitecture() that respects macOS's rosetta2
|
||||
QSFERA_SYNC_EXPORT QString currentCpuArch();
|
||||
} // Utility namespace
|
||||
/** @} */ // \addtogroup
|
||||
|
||||
constexpr bool Utility::isWindows()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr bool Utility::isMac()
|
||||
{
|
||||
#ifdef Q_OS_MAC
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr bool Utility::isUnix()
|
||||
{
|
||||
#ifdef Q_OS_UNIX
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr bool Utility::isLinux()
|
||||
{
|
||||
#if defined(Q_OS_LINUX)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr bool Utility::isBSD()
|
||||
{
|
||||
#if defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
} // OCC namespace
|
||||
|
||||
QSFERA_SYNC_EXPORT QDebug operator<<(QDebug debug, std::chrono::nanoseconds in);
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Helper macro for enumToDisplayName(), it enables us to handle plain C++ enum values
|
||||
* @param Enum an enum value
|
||||
*/
|
||||
#define OCC_UTIL_ENUM_DISPLAY_NAME(Enum) \
|
||||
case Enum: \
|
||||
return QStringLiteral(#Enum);
|
||||
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@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 "result.h"
|
||||
#include "utility.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
#include <QtMacExtras/QtMacExtras>
|
||||
#endif
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <CoreServices/CoreServices.h>
|
||||
#import <Foundation/NSFileManager.h>
|
||||
#import <Foundation/NSUserDefaults.h>
|
||||
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
void Utility::setupFavLink(const QString &folder)
|
||||
{
|
||||
// Finder: Place under "Places"/"Favorites" on the left sidebar
|
||||
CFStringRef folderCFStr = CFStringCreateWithCString(nullptr, folder.toUtf8().data(), kCFStringEncodingUTF8);
|
||||
QScopeGuard freeFolder([folderCFStr]() { CFRelease(folderCFStr); });
|
||||
|
||||
CFURLRef urlRef = CFURLCreateWithFileSystemPath(nullptr, folderCFStr, kCFURLPOSIXPathStyle, true);
|
||||
QScopeGuard freeUrl([urlRef]() { CFRelease(urlRef); });
|
||||
|
||||
LSSharedFileListRef placesItems = LSSharedFileListCreate(nullptr, kLSSharedFileListFavoriteItems, nullptr);
|
||||
QScopeGuard freePlaces([placesItems]() { CFRelease(placesItems); });
|
||||
|
||||
if (placesItems) {
|
||||
// Insert an item to the list.
|
||||
if (LSSharedFileListItemRef item = LSSharedFileListInsertItemURL(placesItems, kLSSharedFileListItemLast, nullptr, nullptr, urlRef, nullptr, nullptr)) {
|
||||
CFRelease(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Utility::hasSystemLaunchOnStartup(const QString &appName)
|
||||
{
|
||||
Q_UNUSED(appName)
|
||||
return false;
|
||||
}
|
||||
|
||||
static Result<void, QString> writePlistToFile(NSString *plistFile, NSDictionary *plist)
|
||||
{
|
||||
NSError *error = nil;
|
||||
|
||||
// Check if the directory exists. On a fresh installation for example, the directory does not
|
||||
// exist, so writing the plist file below will fail.
|
||||
QDir plistDir = QFileInfo(QString::fromNSString(plistFile)).dir();
|
||||
if (!plistDir.exists()) {
|
||||
if (!QDir().mkpath(plistDir.path())) {
|
||||
return QString(QStringLiteral("Failed to create directory '%1'")).arg(plistDir.path());
|
||||
}
|
||||
|
||||
// Permissions always seem to be 0700, so set that.
|
||||
// Note: the Permission enum documentation states that on unix the owner permissions are
|
||||
// returned, but: "This behavior might change in a future Qt version." So we play it safe,
|
||||
// and set both the user and the owner permissions to rwx.
|
||||
if (!QFile(plistDir.path())
|
||||
.setPermissions(QFileDevice::ReadOwner | QFileDevice::ReadUser | QFileDevice::WriteOwner | QFileDevice::WriteUser | QFileDevice::ExeOwner
|
||||
| QFileDevice::ExeUser)) {
|
||||
qCInfo(lcUtility()) << "Failed to set directory permmissions for" << plistDir.path();
|
||||
}
|
||||
}
|
||||
|
||||
// Now write the file.
|
||||
qCInfo(lcUtility()) << "Writing plist file"
|
||||
<< QString::fromNSString(plistFile); // Especially for branded clients: log the file name, so it can be found when debugging.
|
||||
if (![plist writeToURL:[NSURL fileURLWithPath:plistFile isDirectory:NO] error:&error]) {
|
||||
return QString::fromNSString(error.localizedDescription);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static Result<NSDictionary *, QString> readPlistFromFile(NSString *plistFile)
|
||||
{
|
||||
NSError *error = nil;
|
||||
if (NSDictionary *plist = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:plistFile isDirectory:NO] error:&error]) {
|
||||
return plist;
|
||||
} else {
|
||||
return QString::fromNSString(error.localizedDescription);
|
||||
}
|
||||
}
|
||||
|
||||
bool Utility::hasLaunchOnStartup(const QString &appName)
|
||||
{
|
||||
Q_UNUSED(appName)
|
||||
|
||||
@autoreleasepool {
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSString *appIdentifier = QCoreApplication::organizationDomain().toNSString();
|
||||
NSString *plistFile = [NSHomeDirectory() stringByAppendingFormat:@"/Library/LaunchAgents/%@.plist", appIdentifier];
|
||||
|
||||
if ([fileManager fileExistsAtPath:plistFile]) {
|
||||
auto maybePlist = readPlistFromFile(plistFile);
|
||||
if (!maybePlist) {
|
||||
qCInfo(lcUtility()).nospace() << "Cannot read '" << QString::fromNSString(plistFile) << "', probably not a valid plist file";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NSDictionary *plist = *maybePlist) {
|
||||
// yes, there is a valid plist file...
|
||||
if (id label = plist[@"Label"]) {
|
||||
// ... with a label...
|
||||
if ([appIdentifier isEqualToString:label]) {
|
||||
// ... and yes, it's the correct app-id...
|
||||
if (id program = plist[@"Program"]) {
|
||||
// .. and there is a program mentioned ...
|
||||
// (Note: case insensitive compare, because most fs setups on mac are case insensitive)
|
||||
if ([QCoreApplication::applicationFilePath().toNSString() compare:program options:NSCaseInsensitiveSearch] == NSOrderedSame) {
|
||||
// ... and it's our executable ..
|
||||
if (NSNumber *value = plist[@"RunAtLoad"]) {
|
||||
// yes, there is even a RunAtLoad key, so use it!
|
||||
return [value boolValue];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static Result<void, QString> writeNewPlistFile(NSString *plistFile, NSString *fullPath, bool enable)
|
||||
{
|
||||
NSDictionary *plistTemplate =
|
||||
@{@"Label" : QCoreApplication::organizationDomain().toNSString(),
|
||||
@"KeepAlive" : @NO,
|
||||
@"Program" : fullPath,
|
||||
@"RunAtLoad" : enable ? @YES : @NO};
|
||||
|
||||
return writePlistToFile(plistFile, plistTemplate);
|
||||
}
|
||||
|
||||
static Result<void, QString> modifyPlist(NSString *plistFile, NSDictionary *plist, bool enable)
|
||||
{
|
||||
if (NSNumber *value = plist[@"RunAtLoad"]) {
|
||||
// ok, there is a key
|
||||
if ([value boolValue] == enable) {
|
||||
// nothing to do
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// now either the key was missing, or it had the wrong value, so set the key and write the plist back
|
||||
NSMutableDictionary *newPlist = [plist mutableCopy];
|
||||
newPlist[@"RunAtLoad"] = enable ? @YES : @NO;
|
||||
return writePlistToFile(plistFile, newPlist);
|
||||
}
|
||||
|
||||
void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName, bool enable)
|
||||
{
|
||||
Q_UNUSED(appName)
|
||||
Q_UNUSED(guiName)
|
||||
|
||||
@autoreleasepool {
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSString *fullPath = QCoreApplication::applicationFilePath().toNSString();
|
||||
NSString *appIdentifier = QCoreApplication::organizationDomain().toNSString();
|
||||
NSString *plistFile = [NSHomeDirectory() stringByAppendingFormat:@"/Library/LaunchAgents/%@.plist", appIdentifier];
|
||||
|
||||
// An error might occur in the code below, but we cannot report anything, so we just ignore them.
|
||||
|
||||
if ([fileManager fileExistsAtPath:plistFile]) {
|
||||
auto maybePlist = readPlistFromFile(plistFile);
|
||||
if (!maybePlist) {
|
||||
// broken plist, overwrite it
|
||||
auto result = writeNewPlistFile(plistFile, fullPath, enable);
|
||||
if (!result) {
|
||||
qCWarning(lcUtility) << Q_FUNC_INFO << result.error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
NSDictionary *plist = *maybePlist;
|
||||
|
||||
id programValue = plist[@"Program"];
|
||||
if (programValue == nil) {
|
||||
// broken plist, overwrite it
|
||||
auto result = writeNewPlistFile(plistFile, fullPath, enable);
|
||||
if (!result) {
|
||||
qCWarning(lcUtility) << result.error();
|
||||
}
|
||||
} else if (![fileManager fileExistsAtPath:programValue]) {
|
||||
// Ok, a plist from some removed program, overwrite it
|
||||
auto result = writeNewPlistFile(plistFile, fullPath, enable);
|
||||
if (!result) {
|
||||
qCWarning(lcUtility) << result.error();
|
||||
}
|
||||
} else if ([fullPath compare:programValue options:NSCaseInsensitiveSearch]
|
||||
== NSOrderedSame) { // (Note: case insensitive compare, because most fs setups on mac are case insensitive)
|
||||
// Wohoo, it's ours! Now carefully change only the RunAtLoad entry. If any value for
|
||||
// e.g. KeepAlive was changed, we leave it as-is.
|
||||
auto result = modifyPlist(plistFile, plist, enable);
|
||||
if (!result) {
|
||||
qCWarning(lcUtility) << result.error();
|
||||
}
|
||||
} else if ([fullPath hasPrefix:@"/Applications/"]) {
|
||||
// ok, we seem to be an officially installed application, overwrite the file
|
||||
auto result = writeNewPlistFile(plistFile, fullPath, enable);
|
||||
if (!result) {
|
||||
qCWarning(lcUtility) << result.error();
|
||||
}
|
||||
} else {
|
||||
qCInfo(lcUtility) << "We're not an installed application, there is anoter executable "
|
||||
"mentioned in the plist file, and that executable seems to exist, "
|
||||
"so let's not touch the file.";
|
||||
}
|
||||
} else {
|
||||
if (enable) {
|
||||
// plist doens't exist, write a new one.
|
||||
auto result = writeNewPlistFile(plistFile, fullPath, enable);
|
||||
if (!result) {
|
||||
qCWarning(lcUtility) << result.error();
|
||||
}
|
||||
} else {
|
||||
// Do nothing: if the file doesn't exist, the client won't be auto-started.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef TOKEN_AUTH_ONLY
|
||||
bool Utility::hasDarkSystray()
|
||||
{
|
||||
@autoreleasepool {
|
||||
if (auto style = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"]) {
|
||||
return [style isEqualToString:@"Dark"];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
QString Utility::currentCpuArch()
|
||||
{
|
||||
static const QString rv = []() {
|
||||
int procTranslated = 0;
|
||||
size_t size = sizeof(procTranslated);
|
||||
|
||||
const auto rv = sysctlbyname("sysctl.proc_translated", &procTranslated, &size, nullptr, 0);
|
||||
|
||||
// should be backed up if not evaluated immediately
|
||||
const auto error = errno;
|
||||
|
||||
qCDebug(lcUtility) << "proc_translated:" << procTranslated;
|
||||
qCDebug(lcUtility) << "error:" << strerror(error);
|
||||
|
||||
if (rv != -1) {
|
||||
if (procTranslated == 1) {
|
||||
qCInfo(lcUtility) << "process translated with rosetta2";
|
||||
return QStringLiteral("arm64");
|
||||
} else {
|
||||
qCInfo(lcUtility) << "process not translated";
|
||||
}
|
||||
} else {
|
||||
if (error == ENOENT) {
|
||||
qCDebug(lcUtility) << "not running on Apple Silicon";
|
||||
} else {
|
||||
qCDebug(lcUtility) << "unknown error trying to detect whether proc is translated";
|
||||
}
|
||||
}
|
||||
|
||||
const auto qtArch = QSysInfo::currentCpuArchitecture();
|
||||
qCDebug(lcUtility) << "CPU arch reported by Qt:" << qtArch;
|
||||
return qtArch;
|
||||
}();
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
* Copyright (C) by Daniel Molkentin <danimo@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 "utility.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QString>
|
||||
#include <QTextStream>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
void Utility::setupFavLink(const QString &folder)
|
||||
{
|
||||
// Nautilus: add to ~/.gtk-bookmarks
|
||||
QFile gtkBookmarks(QDir::homePath() + QLatin1String("/.config/gtk-3.0/bookmarks"));
|
||||
const QByteArray folderUrl = QUrl::fromLocalFile(folder).toEncoded(QUrl::ComponentFormattingOption::EncodeSpaces);
|
||||
if (gtkBookmarks.open(QFile::ReadWrite)) {
|
||||
QByteArray places = gtkBookmarks.readAll();
|
||||
if (!places.contains(folderUrl)) {
|
||||
places += folderUrl;
|
||||
gtkBookmarks.reset();
|
||||
gtkBookmarks.write(places + '\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns the autostart directory the linux way
|
||||
// and respects the XDG_CONFIG_HOME env variable
|
||||
static QString getUserAutostartDir()
|
||||
{
|
||||
QString config = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
|
||||
config += QLatin1String("/autostart/");
|
||||
return config;
|
||||
}
|
||||
|
||||
bool Utility::hasSystemLaunchOnStartup(const QString &appName)
|
||||
{
|
||||
Q_UNUSED(appName)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Utility::hasLaunchOnStartup(const QString &appName)
|
||||
{
|
||||
QString desktopFileLocation = getUserAutostartDir() + appName + QLatin1String(".desktop");
|
||||
return QFile::exists(desktopFileLocation);
|
||||
}
|
||||
|
||||
void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName, bool enable)
|
||||
{
|
||||
QString userAutoStartPath = getUserAutostartDir();
|
||||
QString desktopFileLocation = userAutoStartPath + appName + QLatin1String(".desktop");
|
||||
if (enable) {
|
||||
if (!QDir().exists(userAutoStartPath) && !QDir().mkpath(userAutoStartPath)) {
|
||||
qCWarning(lcUtility) << u"Could not create autostart folder" << userAutoStartPath;
|
||||
return;
|
||||
}
|
||||
QFile iniFile(desktopFileLocation);
|
||||
if (!iniFile.open(QIODevice::WriteOnly)) {
|
||||
qCWarning(lcUtility) << u"Could not write auto start entry" << desktopFileLocation;
|
||||
return;
|
||||
}
|
||||
|
||||
auto autostartApplicationPath = []() -> QString {
|
||||
// $APPIMAGE will be set to the AppImage's path by the AppImage runtime
|
||||
// if it is set, we can assume to be run from within an AppImage
|
||||
// in that case, the desktop file should point to the AppImage rather than the
|
||||
// main binary, which will be in a temporary mount point
|
||||
if (Utility::runningInAppImage()) {
|
||||
return Utility::appImageLocation();
|
||||
}
|
||||
|
||||
// we need to launch the client via its provided AppRun script (or at least source the AppRun hooks) to make sure it is displayed correctly
|
||||
// if installed as a native package generated by linuxdeploy-plugin-native_packages, a linuxdeploy.conf file will be available that points to the
|
||||
// location of the installed AppDir
|
||||
QString linuxdeployConfPath = qApp->applicationDirPath() + QStringLiteral("/linuxdeploy.conf");
|
||||
|
||||
if (QFile(linuxdeployConfPath).exists()) {
|
||||
QSettings linuxdeployConf(linuxdeployConfPath, QSettings::IniFormat);
|
||||
|
||||
const auto appdirInstallatedPath = linuxdeployConf.value(QStringLiteral("native_packages/appdir_installed_path"));
|
||||
if (!appdirInstallatedPath.isNull()) {
|
||||
return appdirInstallatedPath.toString() + QStringLiteral("/AppRun");
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, we just use the application binary's own path, which should work for distribution-packaged installations and also for development
|
||||
return QCoreApplication::applicationFilePath();
|
||||
}();
|
||||
|
||||
QTextStream ts(&iniFile);
|
||||
ts.setEncoding(QStringConverter::Utf8);
|
||||
ts << QStringLiteral("[Desktop Entry]\n" //
|
||||
"Name=%1\n" //
|
||||
"GenericName=File Synchronizer\n" //
|
||||
"Exec=%2\n" //
|
||||
"Terminal=false\n" //
|
||||
"Icon=%3\n" //
|
||||
"Categories=Network\n" //
|
||||
"Type=Application\n" //
|
||||
"StartupNotify=false\n" //
|
||||
"X-GNOME-Autostart-enabled=true\n" //
|
||||
"X-GNOME-Autostart-Delay=10")
|
||||
.arg(guiName, autostartApplicationPath, appName.toLower());
|
||||
} else {
|
||||
if (!QFile::remove(desktopFileLocation)) {
|
||||
qCWarning(lcUtility) << u"Could not remove autostart desktop file";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Utility::hasDarkSystray()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@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 "utility_win.h"
|
||||
#include "utility.h"
|
||||
|
||||
#include "libsync/common/asserts.h"
|
||||
#include "libsync/common/filesystembase.h"
|
||||
#include "libsync/filesystem.h"
|
||||
|
||||
#include <comdef.h>
|
||||
#include <qt_windows.h>
|
||||
#include <shlguid.h>
|
||||
#include <shlobj.h>
|
||||
#include <string>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QSettings>
|
||||
|
||||
namespace {
|
||||
const QString systemRunPathC()
|
||||
{
|
||||
return QStringLiteral("HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Run");
|
||||
}
|
||||
|
||||
const QString runPathC()
|
||||
{
|
||||
return QStringLiteral("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run");
|
||||
}
|
||||
|
||||
const QString systemThemesC()
|
||||
{
|
||||
return QStringLiteral("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
void Utility::setupFavLink(const QString &) { }
|
||||
|
||||
bool Utility::hasSystemLaunchOnStartup(const QString &appName)
|
||||
{
|
||||
QSettings settings(systemRunPathC(), QSettings::NativeFormat);
|
||||
return settings.contains(appName);
|
||||
}
|
||||
|
||||
bool Utility::hasLaunchOnStartup(const QString &appName)
|
||||
{
|
||||
QSettings settings(runPathC(), QSettings::NativeFormat);
|
||||
return settings.contains(appName);
|
||||
}
|
||||
|
||||
void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName, bool enable)
|
||||
{
|
||||
Q_UNUSED(guiName)
|
||||
QSettings settings(runPathC(), QSettings::NativeFormat);
|
||||
if (enable) {
|
||||
settings.setValue(appName, QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
|
||||
} else {
|
||||
settings.remove(appName);
|
||||
}
|
||||
}
|
||||
|
||||
bool Utility::hasDarkSystray()
|
||||
{
|
||||
const QSettings settings(systemThemesC(), QSettings::NativeFormat);
|
||||
return !settings.value(QStringLiteral("SystemUsesLightTheme"), false).toBool();
|
||||
}
|
||||
|
||||
|
||||
void Utility::UnixTimeToLargeIntegerFiletime(time_t t, LARGE_INTEGER *hundredNSecs)
|
||||
{
|
||||
hundredNSecs->QuadPart = FileSystem::time_tToFileTime(t).time_since_epoch().count();
|
||||
}
|
||||
|
||||
QString Utility::formatWinError(long errorCode)
|
||||
{
|
||||
return QStringLiteral("WindowsError: 0x%1: %2")
|
||||
.arg(QString::number(static_cast<ulong>(errorCode), 16), QString::fromWCharArray(_com_error(errorCode).ErrorMessage()));
|
||||
}
|
||||
|
||||
Utility::Handle::Handle(HANDLE h, std::function<void(HANDLE)> &&close, uint32_t error)
|
||||
: _handle(h)
|
||||
, _close(std::move(close))
|
||||
, _error(error)
|
||||
{
|
||||
if (_handle == INVALID_HANDLE_VALUE && _error == NO_ERROR) {
|
||||
_error = GetLastError();
|
||||
}
|
||||
}
|
||||
|
||||
Utility::Handle Utility::Handle::createHandle(const std::filesystem::path &path, const CreateHandleParameter &p)
|
||||
{
|
||||
uint32_t flags = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS;
|
||||
if (!p.followSymlinks) {
|
||||
flags |= FILE_FLAG_OPEN_REPARSE_POINT;
|
||||
}
|
||||
if (p.async) {
|
||||
flags |= FILE_FLAG_OVERLAPPED;
|
||||
}
|
||||
auto handle = Utility::Handle{CreateFileW(path.native().data(), p.accessMode, p.shareMode, nullptr, p.creationFlags, flags, nullptr)};
|
||||
handle._path = path;
|
||||
return handle;
|
||||
}
|
||||
|
||||
Utility::Handle::Handle(HANDLE h)
|
||||
: Handle(h, &CloseHandle)
|
||||
{
|
||||
}
|
||||
|
||||
Utility::Handle::~Handle()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
void Utility::Handle::close()
|
||||
{
|
||||
if (_handle != INVALID_HANDLE_VALUE) {
|
||||
_close(_handle);
|
||||
_handle = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Utility::Handle::error() const
|
||||
{
|
||||
return _error;
|
||||
}
|
||||
|
||||
bool Utility::Handle::hasError() const
|
||||
{
|
||||
return _error != NO_ERROR;
|
||||
}
|
||||
|
||||
const std::filesystem::path &Utility::Handle::path() const
|
||||
{
|
||||
return _path;
|
||||
}
|
||||
|
||||
QString Utility::Handle::errorMessage() const
|
||||
{
|
||||
return formatWinError(_error);
|
||||
}
|
||||
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include <QString>
|
||||
#include <qt_windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace OCC {
|
||||
namespace Utility {
|
||||
class QSFERA_SYNC_EXPORT Handle
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* A RAAI for Windows Handles
|
||||
*/
|
||||
Handle() = default;
|
||||
explicit Handle(HANDLE h);
|
||||
explicit Handle(HANDLE h, std::function<void(HANDLE)> &&close, uint32_t error = NO_ERROR);
|
||||
|
||||
struct CreateHandleParameter
|
||||
{
|
||||
uint32_t accessMode = 0;
|
||||
uint32_t shareMode = FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;
|
||||
uint32_t creationFlags = OPEN_EXISTING;
|
||||
bool followSymlinks = true;
|
||||
bool async = false;
|
||||
};
|
||||
static Handle createHandle(const std::filesystem::path &path, const CreateHandleParameter &p = {});
|
||||
|
||||
Handle(const Handle &) = delete;
|
||||
Handle &operator=(const Handle &) = delete;
|
||||
|
||||
Handle(Handle &&other)
|
||||
{
|
||||
std::swap(_handle, other._handle);
|
||||
std::swap(_close, other._close);
|
||||
}
|
||||
|
||||
Handle &operator=(Handle &&other)
|
||||
{
|
||||
if (this != &other) {
|
||||
std::swap(_handle, other._handle);
|
||||
std::swap(_close, other._close);
|
||||
std::swap(_error, other._error);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~Handle();
|
||||
|
||||
const HANDLE &handle() const { return _handle; }
|
||||
|
||||
void close();
|
||||
|
||||
explicit operator bool() const { return _handle != INVALID_HANDLE_VALUE; }
|
||||
|
||||
operator HANDLE() const { return _handle; }
|
||||
|
||||
HANDLE release() { return std::exchange(_handle, INVALID_HANDLE_VALUE); }
|
||||
|
||||
uint32_t error() const;
|
||||
bool hasError() const;
|
||||
|
||||
/**
|
||||
*
|
||||
* Returns the file system path associated with this handle, if it was set during handle creation.
|
||||
*
|
||||
* This can be useful for error reporting, logging, or debugging, as it allows you to identify
|
||||
* the file or resource that the handle refers to. If no path was set, the returned path may be empty.
|
||||
*
|
||||
* @return Reference to the associated std::filesystem::path, or an empty path if not set.
|
||||
*/
|
||||
const std::filesystem::path &path() const;
|
||||
|
||||
QString errorMessage() const;
|
||||
|
||||
private:
|
||||
HANDLE _handle = INVALID_HANDLE_VALUE;
|
||||
std::function<void(HANDLE)> _close;
|
||||
uint32_t _error = NO_ERROR;
|
||||
std::filesystem::path _path;
|
||||
};
|
||||
|
||||
QSFERA_SYNC_EXPORT void UnixTimeToLargeIntegerFiletime(time_t t, LARGE_INTEGER *hundredNSecs);
|
||||
|
||||
QSFERA_SYNC_EXPORT QString formatWinError(long error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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 "common/version.h"
|
||||
|
||||
const QVersionNumber &OCC::Version::version()
|
||||
{
|
||||
static const auto v = QVersionNumber({ @MIRALL_VERSION_MAJOR@, @MIRALL_VERSION_MINOR@, @MIRALL_VERSION_PATCH@ });
|
||||
return v;
|
||||
}
|
||||
|
||||
const QVersionNumber &OCC::Version::versionWithBuildNumber()
|
||||
{
|
||||
static const auto v = QVersionNumber({ @MIRALL_VERSION_MAJOR@, @MIRALL_VERSION_MINOR@, @MIRALL_VERSION_PATCH@, @MIRALL_VERSION_BUILD@ });
|
||||
return v;
|
||||
}
|
||||
|
||||
int OCC::Version::buildNumber()
|
||||
{
|
||||
return versionWithBuildNumber().segmentAt(3);
|
||||
}
|
||||
|
||||
QString OCC::Version::gitSha()
|
||||
{
|
||||
return QStringLiteral("@GIT_SHA1@");
|
||||
}
|
||||
|
||||
QString OCC::Version::displayString()
|
||||
{
|
||||
// ensure a well-defined version string
|
||||
constexpr std::string_view versionString("@MIRALL_VERSION_STRING@");
|
||||
static_assert(versionString.find("@MIRALL_VERSION_MAJOR@.@MIRALL_VERSION_MINOR@.@MIRALL_VERSION_PATCH@") == 0);
|
||||
return QStringLiteral("@MIRALL_VERSION_STRING@");
|
||||
}
|
||||
|
||||
bool OCC::Version::isBeta()
|
||||
{
|
||||
#cmakedefine01 BETA_CHANNEL_BUILD
|
||||
return BETA_CHANNEL_BUILD;
|
||||
}
|
||||
|
||||
bool OCC::Version::withUpdateNotification(){
|
||||
#cmakedefine01 WITH_UPDATE_NOTIFICATION
|
||||
return WITH_UPDATE_NOTIFICATION;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QVersionNumber>
|
||||
|
||||
namespace OCC::Version {
|
||||
QSFERA_SYNC_EXPORT const QVersionNumber &version();
|
||||
|
||||
QSFERA_SYNC_EXPORT const QVersionNumber &versionWithBuildNumber();
|
||||
|
||||
QSFERA_SYNC_EXPORT int buildNumber();
|
||||
|
||||
/**
|
||||
* The commit id
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT QString gitSha();
|
||||
|
||||
QSFERA_SYNC_EXPORT QString displayString();
|
||||
|
||||
QSFERA_SYNC_EXPORT bool isBeta();
|
||||
|
||||
QSFERA_SYNC_EXPORT bool withUpdateNotification();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#cmakedefine01 WITH_CRASHREPORTER
|
||||
|
||||
#cmakedefine THEME_CLASS @THEME_CLASS@
|
||||
#cmakedefine THEME_INCLUDE "@THEME_INCLUDE@"
|
||||
|
||||
#cmakedefine APPLICATION_NAME "@APPLICATION_NAME@"
|
||||
#cmakedefine APPLICATION_VENDOR "@APPLICATION_VENDOR@"
|
||||
#cmakedefine APPLICATION_REV_DOMAIN "@APPLICATION_REV_DOMAIN@"
|
||||
#cmakedefine APPLICATION_SHORTNAME "@APPLICATION_SHORTNAME@"
|
||||
#cmakedefine APPLICATION_EXECUTABLE "@APPLICATION_EXECUTABLE@"
|
||||
#cmakedefine APPLICATION_UPDATE_URL "@APPLICATION_UPDATE_URL@"
|
||||
#cmakedefine APPLICATION_DEFAULT_SERVER_URL "@APPLICATION_DEFAULT_SERVER_URL@"
|
||||
#cmakedefine APPLICATION_ICON_NAME "@APPLICATION_ICON_NAME@"
|
||||
@@ -0,0 +1,520 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "configfile.h"
|
||||
#include "common/asserts.h"
|
||||
#include "common/utility.h"
|
||||
#include "common/version.h"
|
||||
#include "libsync/globalconfig.h"
|
||||
#include "logger.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include "creds/abstractcredentials.h"
|
||||
|
||||
#include "csync_exclude.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QHeaderView>
|
||||
#include <QLoggingCategory>
|
||||
#include <QOperatingSystemVersion>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <chrono>
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
namespace chrono = std::chrono;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcConfigFile, "sync.configfile", QtInfoMsg)
|
||||
namespace {
|
||||
const QString logHttpC() { return QStringLiteral("logHttp"); }
|
||||
const QString remotePollIntervalC()
|
||||
{
|
||||
return QStringLiteral("remotePollInterval");
|
||||
}
|
||||
|
||||
const QString fullLocalDiscoveryIntervalC()
|
||||
{
|
||||
return QStringLiteral("fullLocalDiscoveryInterval");
|
||||
}
|
||||
|
||||
const QString crashReporterC()
|
||||
{
|
||||
return QStringLiteral("crashReporter");
|
||||
}
|
||||
const QString skipUpdateCheckC() { return QStringLiteral("skipUpdateCheck"); }
|
||||
const QString updateCheckIntervalC() { return QStringLiteral("updateCheckInterval"); }
|
||||
const QString updateChannelC() { return QStringLiteral("updateChannel"); }
|
||||
const QString uiLanguageC() { return QStringLiteral("uiLanguage"); }
|
||||
const QString geometryC() { return QStringLiteral("geometry"); }
|
||||
const QString timeoutC()
|
||||
{
|
||||
return QStringLiteral("timeout");
|
||||
}
|
||||
const QString automaticLogDirC() { return QStringLiteral("logToTemporaryLogDir"); }
|
||||
const QString numberOfLogsToKeepC()
|
||||
{
|
||||
return QStringLiteral("numberOfLogsToKeep");
|
||||
}
|
||||
|
||||
// The key `clientVersion` stores the version *with* build number of the config file. It is named
|
||||
// this way, because before 5.0, only the version *without* build number was stored.
|
||||
const QString clientVersionC() { return QStringLiteral("clientVersion"); }
|
||||
|
||||
const QString useUploadLimitC() { return QStringLiteral("BWLimit/useUploadLimit"); }
|
||||
const QString useDownloadLimitC() { return QStringLiteral("BWLimit/useDownloadLimit"); }
|
||||
const QString uploadLimitC() { return QStringLiteral("BWLimit/uploadLimit"); }
|
||||
const QString downloadLimitC() { return QStringLiteral("BWLimit/downloadLimit"); }
|
||||
|
||||
const QString pauseSyncWhenMeteredC()
|
||||
{
|
||||
return QStringLiteral("pauseWhenMetered");
|
||||
}
|
||||
const QString moveToTrashC() { return QStringLiteral("moveToTrash"); }
|
||||
|
||||
const QString issuesWidgetFilterC()
|
||||
{
|
||||
return QStringLiteral("issuesWidgetFilter");
|
||||
}
|
||||
|
||||
QString excludeFileNameC()
|
||||
{
|
||||
return QStringLiteral("sync-exclude.lst");
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
QString ConfigFile::_confDir = QString();
|
||||
const std::chrono::seconds DefaultRemotePollInterval { 30 };
|
||||
|
||||
static chrono::milliseconds millisecondsValue(const QSettings &setting, const QString &key,
|
||||
chrono::milliseconds defaultValue)
|
||||
{
|
||||
return chrono::milliseconds(setting.value(key, qlonglong(defaultValue.count())).toLongLong());
|
||||
}
|
||||
|
||||
ConfigFile::ConfigFile()
|
||||
{
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
}
|
||||
|
||||
bool ConfigFile::setConfDir(const QString &value)
|
||||
{
|
||||
QString dirPath = value;
|
||||
if (dirPath.isEmpty())
|
||||
return false;
|
||||
|
||||
QFileInfo fi(dirPath);
|
||||
if (!fi.exists()) {
|
||||
QDir().mkpath(dirPath);
|
||||
fi.setFile(dirPath);
|
||||
}
|
||||
if (fi.exists() && fi.isDir()) {
|
||||
dirPath = fi.absoluteFilePath();
|
||||
qCInfo(lcConfigFile) << u"Using custom config dir " << dirPath;
|
||||
_confDir = dirPath;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<QStringList> ConfigFile::issuesWidgetFilter() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
if (settings.contains(issuesWidgetFilterC())) {
|
||||
return settings.value(issuesWidgetFilterC()).toStringList();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void ConfigFile::setIssuesWidgetFilter(const QStringList &checked)
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
settings.setValue(issuesWidgetFilterC(), checked);
|
||||
settings.sync();
|
||||
}
|
||||
|
||||
std::chrono::seconds ConfigFile::timeout() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
const auto val = settings.value(timeoutC()).toInt(); // default to 5 min
|
||||
return val ? std::chrono::seconds(val) : 5min;
|
||||
}
|
||||
|
||||
void ConfigFile::saveGeometry(QWidget *w)
|
||||
{
|
||||
OC_ASSERT(!w->objectName().isNull());
|
||||
auto settings = makeQSettings();
|
||||
settings.beginGroup(w->objectName());
|
||||
settings.setValue(geometryC(), w->saveGeometry());
|
||||
settings.sync();
|
||||
}
|
||||
|
||||
void ConfigFile::restoreGeometry(QWidget *w)
|
||||
{
|
||||
w->restoreGeometry(getValue(QStringLiteral("%1/%2").arg(geometryC(), w->objectName())).toByteArray());
|
||||
}
|
||||
|
||||
void ConfigFile::saveGeometryHeader(QHeaderView *header)
|
||||
{
|
||||
if (!header)
|
||||
return;
|
||||
OC_ASSERT(!header->objectName().isEmpty());
|
||||
|
||||
auto settings = makeQSettings();
|
||||
settings.beginGroup(header->objectName());
|
||||
settings.setValue(geometryC(), header->saveState());
|
||||
settings.sync();
|
||||
}
|
||||
|
||||
bool ConfigFile::restoreGeometryHeader(QHeaderView *header)
|
||||
{
|
||||
Q_ASSERT(header && !header->objectName().isNull());
|
||||
|
||||
auto settings = makeQSettings();
|
||||
settings.beginGroup(header->objectName());
|
||||
if (settings.contains(geometryC())) {
|
||||
header->restoreState(settings.value(geometryC()).toByteArray());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ConfigFile::configPath()
|
||||
{
|
||||
if (_confDir.isEmpty()) {
|
||||
// On Unix, use the AppConfigLocation for the settings, that's configurable with the XDG_CONFIG_HOME env variable.
|
||||
// On Windows, use AppDataLocation, that's where the roaming data is and where we should store the config file
|
||||
_confDir = QStandardPaths::writableLocation(Utility::isWindows() ? QStandardPaths::AppDataLocation : QStandardPaths::AppConfigLocation);
|
||||
}
|
||||
QString dir = _confDir;
|
||||
|
||||
if (!dir.endsWith(QLatin1Char('/')))
|
||||
dir.append(QLatin1Char('/'));
|
||||
return dir;
|
||||
}
|
||||
|
||||
QString ConfigFile::excludeFile(Scope scope) const
|
||||
{
|
||||
switch (scope) {
|
||||
case UserScope:
|
||||
return configPath() + excludeFileNameC();
|
||||
case SystemScope:
|
||||
return ConfigFile::defaultExcludeFile();
|
||||
}
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
||||
QString ConfigFile::defaultExcludeFile()
|
||||
{
|
||||
return QStringLiteral(":/client/QSfera/theme/universal/%1").arg(excludeFileNameC());
|
||||
}
|
||||
|
||||
QString ConfigFile::backup() const
|
||||
{
|
||||
QString baseFile = configFile();
|
||||
auto versionString = clientVersionWithBuildNumberString();
|
||||
if (!versionString.isEmpty())
|
||||
versionString.prepend(QLatin1Char('_'));
|
||||
const QString backupFile =
|
||||
QStringLiteral("%1.backup_%2%3")
|
||||
.arg(baseFile, QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss")), versionString);
|
||||
|
||||
// If this exact file already exists it's most likely that a backup was
|
||||
// already done. (two backup calls directly after each other, potentially
|
||||
// even with source alterations in between!)
|
||||
if (!QFile::exists(backupFile)) {
|
||||
QFile f(baseFile);
|
||||
f.copy(backupFile);
|
||||
}
|
||||
return backupFile;
|
||||
}
|
||||
|
||||
QString ConfigFile::configFile()
|
||||
{
|
||||
return configPath() + Theme::instance()->configFileName();
|
||||
}
|
||||
|
||||
QSettings ConfigFile::makeQSettings()
|
||||
{
|
||||
return {configFile(), QSettings::IniFormat};
|
||||
}
|
||||
|
||||
std::unique_ptr<QSettings> ConfigFile::makeQSettingsPointer()
|
||||
{
|
||||
return std::unique_ptr<QSettings>(new QSettings(makeQSettings()));
|
||||
}
|
||||
|
||||
bool ConfigFile::exists()
|
||||
{
|
||||
return QFileInfo::exists(configFile());
|
||||
}
|
||||
|
||||
|
||||
chrono::milliseconds ConfigFile::remotePollInterval(std::chrono::seconds defaultVal) const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
|
||||
auto defaultPollInterval { DefaultRemotePollInterval };
|
||||
|
||||
// The server default-capabilities was set to 60 in some server releases,
|
||||
// which, if interpreted in milliseconds, is pretty small.
|
||||
// If the value is above 5 seconds, it was set intentionally.
|
||||
// Server admins have to set the value in Milliseconds!
|
||||
// i.e. set to greater than 5000 milliseconds on the server to be effective.
|
||||
if (defaultVal > chrono::seconds(5)) {
|
||||
defaultPollInterval = defaultVal;
|
||||
}
|
||||
auto remoteInterval = millisecondsValue(settings, remotePollIntervalC(), defaultPollInterval);
|
||||
if (remoteInterval < chrono::seconds(5)) {
|
||||
remoteInterval = defaultPollInterval;
|
||||
qCWarning(lcConfigFile) << u"Remote Interval is less than 5 seconds, reverting to" << remoteInterval.count();
|
||||
}
|
||||
return remoteInterval;
|
||||
}
|
||||
|
||||
chrono::milliseconds OCC::ConfigFile::fullLocalDiscoveryInterval() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
return millisecondsValue(settings, fullLocalDiscoveryIntervalC(), 1h);
|
||||
}
|
||||
|
||||
chrono::milliseconds ConfigFile::updateCheckInterval() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
|
||||
auto defaultInterval = chrono::hours(10);
|
||||
auto interval = millisecondsValue(settings, updateCheckIntervalC(), defaultInterval);
|
||||
|
||||
auto minInterval = chrono::minutes(5);
|
||||
if (interval < minInterval) {
|
||||
qCWarning(lcConfigFile) << u"Update check interval less than five minutes, resetting to 5 minutes";
|
||||
interval = minInterval;
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
|
||||
bool ConfigFile::skipUpdateCheck() const
|
||||
{
|
||||
const auto fallback = getValue(skipUpdateCheckC(), false);
|
||||
#ifdef Q_OS_WIN
|
||||
return GlobalConfig::getPolicySetting(skipUpdateCheckC(), fallback).toBool();
|
||||
#else
|
||||
return fallback.toBool();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ConfigFile::setSkipUpdateCheck(bool skip)
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
settings.setValue(skipUpdateCheckC(), QVariant(skip));
|
||||
settings.sync();
|
||||
}
|
||||
|
||||
QString ConfigFile::updateChannel() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
return settings.value(updateChannelC(), OCC::Version::isBeta() ? u"stable"_s : u"beta"_s).toString();
|
||||
}
|
||||
|
||||
void ConfigFile::setUpdateChannel(const QString &channel)
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
settings.setValue(updateChannelC(), channel);
|
||||
}
|
||||
|
||||
QString ConfigFile::uiLanguage() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
return settings.value(uiLanguageC(), QString()).toString();
|
||||
}
|
||||
|
||||
void ConfigFile::setUiLanguage(const QString &uiLanguage)
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
settings.setValue(uiLanguageC(), uiLanguage);
|
||||
}
|
||||
|
||||
QVariant ConfigFile::getValue(const QString ¶m, const QVariant &defaultValue) const
|
||||
{
|
||||
auto setting = makeQSettings().value(param);
|
||||
if (!setting.isValid()) {
|
||||
return GlobalConfig::getValue(param, defaultValue);
|
||||
}
|
||||
return setting;
|
||||
}
|
||||
|
||||
void ConfigFile::setValue(const QString &key, const QVariant &value)
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
|
||||
settings.setValue(key, value);
|
||||
}
|
||||
|
||||
int ConfigFile::useUploadLimit() const
|
||||
{
|
||||
return getValue(useUploadLimitC(), 0).toInt();
|
||||
}
|
||||
|
||||
int ConfigFile::useDownloadLimit() const
|
||||
{
|
||||
return getValue(useDownloadLimitC(), 0).toInt();
|
||||
}
|
||||
|
||||
void ConfigFile::setUseUploadLimit(int val)
|
||||
{
|
||||
setValue(useUploadLimitC(), val);
|
||||
}
|
||||
|
||||
void ConfigFile::setUseDownloadLimit(int val)
|
||||
{
|
||||
setValue(useDownloadLimitC(), val);
|
||||
}
|
||||
|
||||
int ConfigFile::uploadLimit() const
|
||||
{
|
||||
return getValue(uploadLimitC(), 10).toInt();
|
||||
}
|
||||
|
||||
int ConfigFile::downloadLimit() const
|
||||
{
|
||||
return getValue(downloadLimitC(), 80).toInt();
|
||||
}
|
||||
|
||||
void ConfigFile::setUploadLimit(int kbytes)
|
||||
{
|
||||
setValue(uploadLimitC(), kbytes);
|
||||
}
|
||||
|
||||
void ConfigFile::setDownloadLimit(int kbytes)
|
||||
{
|
||||
setValue(downloadLimitC(), kbytes);
|
||||
}
|
||||
|
||||
bool ConfigFile::pauseSyncWhenMetered() const
|
||||
{
|
||||
return getValue(pauseSyncWhenMeteredC(), false).toBool();
|
||||
}
|
||||
|
||||
void ConfigFile::setPauseSyncWhenMetered(bool isChecked)
|
||||
{
|
||||
setValue(pauseSyncWhenMeteredC(), isChecked);
|
||||
}
|
||||
|
||||
bool ConfigFile::moveToTrash() const
|
||||
{
|
||||
if (Theme::instance()->enableMoveToTrash()) {
|
||||
return getValue(moveToTrashC(), false).toBool();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ConfigFile::setMoveToTrash(bool isChecked)
|
||||
{
|
||||
setValue(moveToTrashC(), isChecked);
|
||||
}
|
||||
|
||||
bool ConfigFile::crashReporter() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
return settings.value(crashReporterC(), true).toBool();
|
||||
}
|
||||
|
||||
void ConfigFile::setCrashReporter(bool enabled)
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
settings.setValue(crashReporterC(), enabled);
|
||||
}
|
||||
|
||||
bool ConfigFile::automaticLogDir() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
return settings.value(automaticLogDirC(), false).toBool();
|
||||
}
|
||||
|
||||
void ConfigFile::setAutomaticLogDir(bool enabled)
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
settings.setValue(automaticLogDirC(), enabled);
|
||||
}
|
||||
|
||||
int ConfigFile::automaticDeleteOldLogs() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
return settings.value(numberOfLogsToKeepC()).toInt();
|
||||
}
|
||||
|
||||
void ConfigFile::setAutomaticDeleteOldLogs(int number)
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
settings.setValue(numberOfLogsToKeepC(), number);
|
||||
}
|
||||
|
||||
void ConfigFile::configureHttpLogging(std::optional<bool> enable)
|
||||
{
|
||||
if (enable == std::nullopt) {
|
||||
enable = logHttp();
|
||||
}
|
||||
|
||||
auto settings = makeQSettings();
|
||||
settings.setValue(logHttpC(), enable.value());
|
||||
|
||||
static const QSet<QString> rule = { QStringLiteral("sync.httplogger=true") };
|
||||
|
||||
if (enable.value()) {
|
||||
Logger::instance()->addLogRule(rule);
|
||||
} else {
|
||||
Logger::instance()->removeLogRule(rule);
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigFile::logHttp() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
return settings.value(logHttpC(), false).toBool();
|
||||
}
|
||||
|
||||
QString ConfigFile::clientVersionWithBuildNumberString() const
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
return settings.value(clientVersionC(), QString()).toString();
|
||||
}
|
||||
|
||||
void ConfigFile::setClientVersionWithBuildNumberString(const QString &version)
|
||||
{
|
||||
auto settings = makeQSettings();
|
||||
settings.setValue(clientVersionC(), version);
|
||||
}
|
||||
|
||||
void ConfigFile::setupDefaultExcludeFilePaths(ExcludedFiles &excludedFiles)
|
||||
{
|
||||
ConfigFile cfg;
|
||||
QString systemList = cfg.excludeFile(ConfigFile::SystemScope);
|
||||
qCInfo(lcConfigFile) << u"Adding system ignore list to csync:" << systemList;
|
||||
excludedFiles.addExcludeFilePath(systemList);
|
||||
|
||||
QString userList = cfg.excludeFile(ConfigFile::UserScope);
|
||||
if (QFile::exists(userList)) {
|
||||
qCInfo(lcConfigFile) << u"Adding user defined ignore list to csync:" << userList;
|
||||
excludedFiles.addExcludeFilePath(userList);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#ifndef CONFIGFILE_H
|
||||
#define CONFIGFILE_H
|
||||
|
||||
#include "qsferasynclib.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
class QWidget;
|
||||
class QHeaderView;
|
||||
class ExcludedFiles;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class AbstractCredentials;
|
||||
|
||||
/**
|
||||
* @brief The ConfigFile class
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT ConfigFile
|
||||
{
|
||||
public:
|
||||
static QString configPath();
|
||||
static QString configFile();
|
||||
static QSettings makeQSettings();
|
||||
static std::unique_ptr<QSettings> makeQSettingsPointer();
|
||||
|
||||
static bool exists();
|
||||
|
||||
ConfigFile();
|
||||
|
||||
enum Scope {
|
||||
UserScope,
|
||||
SystemScope
|
||||
};
|
||||
|
||||
QString excludeFile(Scope scope) const;
|
||||
static QString defaultExcludeFile(); // doesn't access config dir
|
||||
|
||||
/**
|
||||
* Creates a backup of the file
|
||||
*
|
||||
* Returns the path of the new backup.
|
||||
*/
|
||||
QString backup() const;
|
||||
|
||||
/* Server poll interval in milliseconds */
|
||||
std::chrono::milliseconds remotePollInterval(std::chrono::seconds defaultVal) const;
|
||||
/* Set poll interval. Value in milliseconds has to be larger than 5000 */
|
||||
void setRemotePollInterval(std::chrono::milliseconds interval);
|
||||
|
||||
/**
|
||||
* Interval in milliseconds within which full local discovery is required
|
||||
*
|
||||
* Use -1 to disable regular full local discoveries.
|
||||
*/
|
||||
std::chrono::milliseconds fullLocalDiscoveryInterval() const;
|
||||
|
||||
bool crashReporter() const;
|
||||
void setCrashReporter(bool enabled);
|
||||
|
||||
/** Whether to set up logging to a temp directory on startup.
|
||||
*
|
||||
* Configured via the log window. Not used if command line sets up logging.
|
||||
*/
|
||||
bool automaticLogDir() const;
|
||||
void setAutomaticLogDir(bool enabled);
|
||||
|
||||
/** Number of log files to keep */
|
||||
int automaticDeleteOldLogs() const;
|
||||
void setAutomaticDeleteOldLogs(int number);
|
||||
|
||||
/** Whether to log http traffic */
|
||||
bool logHttp() const;
|
||||
|
||||
/**
|
||||
* Set up HTTP logging.
|
||||
* This method should be called during application startup to make sure no messages are missed.
|
||||
*/
|
||||
void configureHttpLogging(std::optional<bool> enable = std::nullopt);
|
||||
|
||||
/** 0: no limit, 1: manual, >0: automatic */
|
||||
int useUploadLimit() const;
|
||||
int useDownloadLimit() const;
|
||||
void setUseUploadLimit(int);
|
||||
void setUseDownloadLimit(int);
|
||||
/** in kbyte/s */
|
||||
int uploadLimit() const;
|
||||
int downloadLimit() const;
|
||||
void setUploadLimit(int kbytes);
|
||||
void setDownloadLimit(int kbytes);
|
||||
|
||||
bool pauseSyncWhenMetered() const;
|
||||
void setPauseSyncWhenMetered(bool isChecked);
|
||||
|
||||
/** If we should move the files deleted on the server in the trash */
|
||||
bool moveToTrash() const;
|
||||
void setMoveToTrash(bool);
|
||||
|
||||
/// Used for testing, so we do not change the user's config file.
|
||||
static bool setConfDir(const QString &value);
|
||||
|
||||
std::optional<QStringList> issuesWidgetFilter() const;
|
||||
void setIssuesWidgetFilter(const QStringList &checked);
|
||||
|
||||
std::chrono::seconds timeout() const;
|
||||
|
||||
void saveGeometry(QWidget *w);
|
||||
void restoreGeometry(QWidget *w);
|
||||
|
||||
// how often the check about new versions runs
|
||||
std::chrono::milliseconds updateCheckInterval() const;
|
||||
|
||||
bool skipUpdateCheck() const;
|
||||
void setSkipUpdateCheck(bool);
|
||||
|
||||
QString updateChannel() const;
|
||||
void setUpdateChannel(const QString &channel);
|
||||
|
||||
QString uiLanguage() const;
|
||||
void setUiLanguage(const QString &uiLanguage);
|
||||
|
||||
void saveGeometryHeader(QHeaderView *header);
|
||||
bool restoreGeometryHeader(QHeaderView *header);
|
||||
|
||||
/** The client version that last used this settings file.
|
||||
Updated by configVersionMigration() at client startup. */
|
||||
QString clientVersionWithBuildNumberString() const;
|
||||
void setClientVersionWithBuildNumberString(const QString &version);
|
||||
|
||||
/// Add the system and user exclude file path to the ExcludedFiles instance.
|
||||
static void setupDefaultExcludeFilePaths(ExcludedFiles &excludedFiles);
|
||||
|
||||
private:
|
||||
QVariant getValue(const QString ¶m, const QVariant &defaultValue = QVariant()) const;
|
||||
void setValue(const QString &key, const QVariant &value);
|
||||
|
||||
static QString _confDir;
|
||||
};
|
||||
}
|
||||
#endif // CONFIGFILE_H
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "cookiejar.h"
|
||||
|
||||
// empty file for moc
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "qsferasynclib.h"
|
||||
|
||||
#include <QNetworkCookieJar>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class QSFERA_SYNC_EXPORT CookieJar : public QNetworkCookieJar
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
using QNetworkCookieJar::QNetworkCookieJar;
|
||||
|
||||
// expose protected functions
|
||||
using QNetworkCookieJar::setAllCookies;
|
||||
using QNetworkCookieJar::allCookies;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include <QLoggingCategory>
|
||||
|
||||
#include "account.h"
|
||||
#include "common/asserts.h"
|
||||
#include "creds/abstractcredentials.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcCredentials, "sync.credentials", QtInfoMsg)
|
||||
|
||||
AbstractCredentials::AbstractCredentials()
|
||||
: _account(nullptr)
|
||||
, _wasFetched(false)
|
||||
{
|
||||
}
|
||||
|
||||
void AbstractCredentials::setAccount(Account *account)
|
||||
{
|
||||
OC_ENFORCE_X(!_account, "should only setAccount once");
|
||||
_account = account;
|
||||
}
|
||||
void AbstractCredentials::checkCredentials(QNetworkReply *) { }
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#ifndef MIRALL_CREDS_ABSTRACT_CREDENTIALS_H
|
||||
#define MIRALL_CREDS_ABSTRACT_CREDENTIALS_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "accessmanager.h"
|
||||
#include "accountfwd.h"
|
||||
#include "qsferasynclib.h"
|
||||
#include <../csync.h>
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
namespace OCC {
|
||||
|
||||
class AbstractNetworkJob;
|
||||
|
||||
class QSFERA_SYNC_EXPORT AbstractCredentials : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AbstractCredentials();
|
||||
// No need for virtual destructor - QObject already has one.
|
||||
|
||||
/** The bound account for the credentials instance.
|
||||
*
|
||||
* Credentials are always used in conjunction with an account.
|
||||
* Calling Account::setCredentials() will call this function.
|
||||
* Credentials only live as long as the underlying account object.
|
||||
*/
|
||||
virtual void setAccount(Account *account);
|
||||
|
||||
virtual AccessManager *createAM() const = 0;
|
||||
|
||||
/** Whether there are credentials that can be used for a connection attempt. */
|
||||
virtual bool ready() const = 0;
|
||||
|
||||
/** Whether fetchFromKeychain() was called before. */
|
||||
bool wasFetched() const { return _wasFetched; }
|
||||
|
||||
/** Trigger (async) fetching of credential information
|
||||
*
|
||||
* Should set _wasFetched = true, and later Q_EMIT fetched() when done.
|
||||
*/
|
||||
virtual void fetchFromKeychain() = 0;
|
||||
|
||||
/** Ask credentials from the user (typically async)
|
||||
*
|
||||
* Should Q_EMIT asked() when done.
|
||||
*/
|
||||
virtual void restartOauth() = 0;
|
||||
|
||||
virtual void checkCredentials(QNetworkReply *reply);
|
||||
virtual void persist() = 0;
|
||||
|
||||
/** Invalidates token used to authorize requests, it will no longer be used.
|
||||
*
|
||||
* For http auth, this would be the session cookie.
|
||||
*
|
||||
* Note that sensitive data (like the password used to acquire the
|
||||
* session cookie) may be retained. See forgetSensitiveData().
|
||||
*
|
||||
* ready() must return false afterwards.
|
||||
*/
|
||||
virtual void invalidateToken() = 0;
|
||||
|
||||
/** Clears out all sensitive data; used for fully signing out users.
|
||||
*
|
||||
* This should always imply invalidateToken() but may go beyond it.
|
||||
*
|
||||
* For http auth, this would clear the session cookie and password.
|
||||
*/
|
||||
virtual void forgetSensitiveData() = 0;
|
||||
|
||||
Q_SIGNALS:
|
||||
/** Emitted when fetchFromKeychain() is done.
|
||||
*
|
||||
* Note that ready() can be true or false, depending on whether there was useful
|
||||
* data in the keychain.
|
||||
*/
|
||||
// TODO: rename
|
||||
void fetched();
|
||||
|
||||
void authenticationStarted();
|
||||
void authenticationFailed();
|
||||
|
||||
/*
|
||||
* Request to log out.
|
||||
* The connected account should be marked as logged out
|
||||
* and no automatic tries to connect should be made.
|
||||
*/
|
||||
void requestLogout();
|
||||
|
||||
protected:
|
||||
Account *_account;
|
||||
bool _wasFetched;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,240 @@
|
||||
#include "credentialmanager.h"
|
||||
|
||||
#include "account.h"
|
||||
#include "configfile.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include "common/asserts.h"
|
||||
#include "common/chronoelapsedtimer.h"
|
||||
|
||||
#include <QCborValue>
|
||||
#include <QLoggingCategory>
|
||||
#include <QTimer>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcCredentialsManager, "sync.credentials.manager", QtDebugMsg)
|
||||
|
||||
namespace {
|
||||
constexpr auto tiemoutC = 5s;
|
||||
QString credentialKeyC()
|
||||
{
|
||||
return QStringLiteral("%1_credentials").arg(Theme::instance()->appName());
|
||||
}
|
||||
|
||||
QString accountKey(const Account *acc)
|
||||
{
|
||||
OC_ASSERT(!acc->url().isEmpty());
|
||||
return QStringLiteral("%1:%2:%3").arg(credentialKeyC(), acc->url().host(), acc->uuid().toString(QUuid::WithoutBraces));
|
||||
}
|
||||
|
||||
QString scope(const CredentialManager *const manager)
|
||||
{
|
||||
return manager->account() ? accountKey(manager->account()) : credentialKeyC();
|
||||
}
|
||||
|
||||
QString scopedKey(const CredentialManager *const manager, const QString &key)
|
||||
{
|
||||
return scope(manager) + QLatin1Char(':') + key;
|
||||
}
|
||||
}
|
||||
|
||||
CredentialManager::CredentialManager(Account *acc)
|
||||
: QObject(acc)
|
||||
, _account(acc)
|
||||
{
|
||||
}
|
||||
|
||||
CredentialManager::CredentialManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
CredentialJob *CredentialManager::get(const QString &key)
|
||||
{
|
||||
qCInfo(lcCredentialsManager) << u"get" << scopedKey(this, key);
|
||||
auto out = new CredentialJob(this, key);
|
||||
out->start();
|
||||
return out;
|
||||
}
|
||||
|
||||
QKeychain::Job *CredentialManager::set(const QString &key, const QVariant &data)
|
||||
{
|
||||
OC_ASSERT(!data.isNull());
|
||||
qCInfo(lcCredentialsManager) << u"set" << scopedKey(this, key);
|
||||
auto writeJob = new QKeychain::WritePasswordJob(Theme::instance()->appName());
|
||||
writeJob->setKey(scopedKey(this, key));
|
||||
|
||||
auto timer = new QTimer(writeJob);
|
||||
timer->setInterval(tiemoutC);
|
||||
|
||||
Utility::ChronoElapsedTimer elapsedTimer;
|
||||
connect(timer, &QTimer::timeout, writeJob,
|
||||
[writeJob, elapsedTimer] { qCWarning(lcCredentialsManager) << u"set" << writeJob->key() << u"has not yet finished." << elapsedTimer; });
|
||||
connect(writeJob, &QKeychain::WritePasswordJob::finished, this, [writeJob, key, elapsedTimer, this] {
|
||||
if (writeJob->error() == QKeychain::NoError) {
|
||||
qCInfo(lcCredentialsManager) << u"added" << writeJob->key() << u"after" << elapsedTimer;
|
||||
// just a list, the values don't matter
|
||||
credentialsList().setValue(key, true);
|
||||
} else {
|
||||
qCWarning(lcCredentialsManager) << u"Failed to set:" << writeJob->key() << writeJob->errorString() << u"after" << elapsedTimer;
|
||||
}
|
||||
});
|
||||
writeJob->setBinaryData(QCborValue::fromVariant(data).toCbor());
|
||||
// start is delayed so we can directly call it
|
||||
writeJob->start();
|
||||
timer->start();
|
||||
|
||||
return writeJob;
|
||||
}
|
||||
|
||||
QKeychain::Job *CredentialManager::remove(const QString &key)
|
||||
{
|
||||
OC_ASSERT(contains(key));
|
||||
// remove immediately to prevent double invocation by clear()
|
||||
credentialsList().remove(key);
|
||||
qCInfo(lcCredentialsManager) << u"del" << scopedKey(this, key);
|
||||
auto keychainJob = new QKeychain::DeletePasswordJob(Theme::instance()->appName());
|
||||
keychainJob->setKey(scopedKey(this, key));
|
||||
connect(keychainJob, &QKeychain::DeletePasswordJob::finished, this, [keychainJob, key, this] {
|
||||
OC_ASSERT(keychainJob->error() != QKeychain::EntryNotFound);
|
||||
if (keychainJob->error() == QKeychain::NoError) {
|
||||
qCInfo(lcCredentialsManager) << u"removed" << scopedKey(this, key);
|
||||
} else {
|
||||
qCWarning(lcCredentialsManager) << u"Failed to remove:" << scopedKey(this, key) << keychainJob->errorString();
|
||||
}
|
||||
});
|
||||
// start is delayed so we can directly call it
|
||||
keychainJob->start();
|
||||
return keychainJob;
|
||||
}
|
||||
|
||||
QVector<QPointer<QKeychain::Job>> CredentialManager::clear(const QString &group)
|
||||
{
|
||||
OC_ENFORCE(_account || !group.isEmpty());
|
||||
const auto keys = knownKeys(group);
|
||||
QVector<QPointer<QKeychain::Job>> out;
|
||||
out.reserve(keys.size());
|
||||
for (const auto &key : keys) {
|
||||
out << remove(key);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const Account *CredentialManager::account() const
|
||||
{
|
||||
return _account;
|
||||
}
|
||||
|
||||
bool CredentialManager::contains(const QString &key) const
|
||||
{
|
||||
return credentialsList().contains(key);
|
||||
}
|
||||
|
||||
QStringList CredentialManager::knownKeys(const QString &group) const
|
||||
{
|
||||
if (group.isEmpty()) {
|
||||
return credentialsList().allKeys();
|
||||
}
|
||||
credentialsList().beginGroup(group);
|
||||
const auto keys = credentialsList().allKeys();
|
||||
QStringList out;
|
||||
out.reserve(keys.size());
|
||||
for (const auto &k : keys) {
|
||||
out.append(group + QLatin1Char('/') + k);
|
||||
}
|
||||
credentialsList().endGroup();
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to lazily create the settings (group).
|
||||
*
|
||||
* IMPORTANT: the underlying storage is a std::unique_ptr, so do *NOT* store this reference anywhere!
|
||||
*/
|
||||
QSettings &CredentialManager::credentialsList() const
|
||||
{
|
||||
// delayed init as scope requires a fully inizialised acc
|
||||
if (!_credentialsList) {
|
||||
_credentialsList = ConfigFile::makeQSettingsPointer();
|
||||
_credentialsList->beginGroup(QStringLiteral("Credentials/") + scope(this));
|
||||
}
|
||||
return *_credentialsList;
|
||||
}
|
||||
|
||||
CredentialJob::CredentialJob(CredentialManager *parent, const QString &key)
|
||||
: QObject(parent)
|
||||
, _key(key)
|
||||
, _parent(parent)
|
||||
{
|
||||
connect(this, &CredentialJob::finished, this, &CredentialJob::deleteLater);
|
||||
}
|
||||
|
||||
QString CredentialJob::errorString() const
|
||||
{
|
||||
return _errorString;
|
||||
}
|
||||
|
||||
const QVariant &CredentialJob::data() const
|
||||
{
|
||||
return _data;
|
||||
}
|
||||
|
||||
QKeychain::Error CredentialJob::error() const
|
||||
{
|
||||
return _error;
|
||||
}
|
||||
|
||||
void CredentialJob::start()
|
||||
{
|
||||
if (!_parent->contains(_key)) {
|
||||
_error = QKeychain::EntryNotFound;
|
||||
// QKeychain is started delayed, Q_EMIT the signal delayed to make sure we are connected
|
||||
qCDebug(lcCredentialsManager) << u"We don't know" << _key << u"skipping retrieval from keychain";
|
||||
QTimer::singleShot(0, this, &CredentialJob::finished);
|
||||
return;
|
||||
}
|
||||
|
||||
_job = new QKeychain::ReadPasswordJob(Theme::instance()->appName());
|
||||
_job->setKey(scopedKey(_parent, _key));
|
||||
connect(_job, &QKeychain::ReadPasswordJob::finished, this, [this] {
|
||||
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
||||
if (_retryOnKeyChainError && (_job->error() == QKeychain::NoBackendAvailable || _job->error() == QKeychain::OtherError)) {
|
||||
// Could be that the backend was not yet available. Wait some extra seconds.
|
||||
// (Issues #4274 and #6522)
|
||||
// (For kwallet, the error is OtherError instead of NoBackendAvailable, maybe a bug in QtKeychain)
|
||||
qCInfo(lcCredentialsManager) << u"Backend unavailable (yet?) Retrying in a few seconds." << _job->errorString();
|
||||
QTimer::singleShot(10s, this, &CredentialJob::start);
|
||||
_retryOnKeyChainError = false;
|
||||
}
|
||||
#endif
|
||||
OC_ASSERT(_job->error() != QKeychain::EntryNotFound);
|
||||
if (_job->error() == QKeychain::NoError) {
|
||||
QCborParserError error;
|
||||
const auto obj = QCborValue::fromCbor(_job->binaryData(), &error);
|
||||
if (error.error != QCborError::NoError) {
|
||||
_error = QKeychain::OtherError;
|
||||
_errorString = tr("Failed to parse credentials %1").arg(error.errorString());
|
||||
return;
|
||||
}
|
||||
_data = obj.toVariant();
|
||||
OC_ASSERT(_data.isValid());
|
||||
} else {
|
||||
qCWarning(lcCredentialsManager) << u"Failed to get password" << scopedKey(_parent, _key) << _job->errorString();
|
||||
_error = _job->error();
|
||||
_errorString = _job->errorString();
|
||||
}
|
||||
Q_EMIT finished();
|
||||
});
|
||||
_job->start();
|
||||
}
|
||||
|
||||
QString CredentialJob::key() const
|
||||
{
|
||||
return _key;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <QSettings>
|
||||
#include <QVariant>
|
||||
|
||||
#include "qsferasynclib.h"
|
||||
|
||||
#include <qt6keychain/keychain.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace OCC {
|
||||
class Account;
|
||||
class CredentialJob;
|
||||
|
||||
class QSFERA_SYNC_EXPORT CredentialManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
// global credentials
|
||||
CredentialManager(QObject *parent);
|
||||
// account related credentials
|
||||
explicit CredentialManager(Account *acc);
|
||||
|
||||
CredentialJob *get(const QString &key);
|
||||
QKeychain::Job *set(const QString &key, const QVariant &data);
|
||||
QKeychain::Job *remove(const QString &key);
|
||||
/**
|
||||
* Delete all credentials asigned with an account
|
||||
*/
|
||||
QVector<QPointer<QKeychain::Job>> clear(const QString &group = {});
|
||||
|
||||
bool contains(const QString &key) const;
|
||||
const Account *account() const;
|
||||
|
||||
private:
|
||||
QSettings &credentialsList() const;
|
||||
|
||||
// TestCredentialManager
|
||||
QStringList knownKeys(const QString &group = {}) const;
|
||||
|
||||
const Account *const _account = nullptr;
|
||||
mutable std::unique_ptr<QSettings> _credentialsList;
|
||||
|
||||
friend class TestCredentialManager;
|
||||
};
|
||||
|
||||
class QSFERA_SYNC_EXPORT CredentialJob : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
QString key() const;
|
||||
|
||||
QKeychain::Error error() const;
|
||||
|
||||
const QVariant &data() const;
|
||||
|
||||
QString errorString() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void finished();
|
||||
|
||||
private:
|
||||
CredentialJob(CredentialManager *parent, const QString &key);
|
||||
void start();
|
||||
|
||||
QString _key;
|
||||
QVariant _data;
|
||||
QKeychain::Error _error = QKeychain::NoError;
|
||||
QString _errorString;
|
||||
bool _retryOnKeyChainError = true;
|
||||
QKeychain::ReadPasswordJob *_job;
|
||||
|
||||
CredentialManager *const _parent;
|
||||
|
||||
friend class CredentialManager;
|
||||
friend class TestCredentialManager;
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@kde.org>
|
||||
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
#include "creds/httpcredentials.h"
|
||||
|
||||
#include "accessmanager.h"
|
||||
#include "account.h"
|
||||
#include "configfile.h"
|
||||
#include "creds/credentialmanager.h"
|
||||
#include "oauth.h"
|
||||
#include "syncengine.h"
|
||||
|
||||
#include <QAuthenticator>
|
||||
#include <QLoggingCategory>
|
||||
#include <QNetworkInformation>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcHttpCredentials, "sync.credentials.http", QtInfoMsg)
|
||||
|
||||
namespace {
|
||||
constexpr int TokenRefreshMaxRetries = 3;
|
||||
constexpr std::chrono::seconds TokenRefreshDefaultTimeout = 30s;
|
||||
const char authenticationFailedC[] = "qsfera-authentication-failed";
|
||||
|
||||
auto refreshTokenKeyC()
|
||||
{
|
||||
return QStringLiteral("http/oauthtoken");
|
||||
}
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class HttpCredentialsAccessManager : public AccessManager
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
HttpCredentialsAccessManager(const HttpCredentials *cred, QObject *parent = nullptr)
|
||||
: AccessManager(parent)
|
||||
, _cred(cred)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override
|
||||
{
|
||||
QNetworkRequest req(request);
|
||||
if (!req.attribute(HttpCredentials::DontAddCredentialsAttribute).toBool()) {
|
||||
if (_cred && !_cred->_accessToken.isEmpty()) {
|
||||
req.setRawHeader("Authorization", "Bearer " + _cred->_accessToken.toUtf8());
|
||||
}
|
||||
}
|
||||
return AccessManager::createRequest(op, req, outgoingData);
|
||||
}
|
||||
|
||||
private:
|
||||
// The credentials object dies along with the account, while the QNAM might
|
||||
// outlive both.
|
||||
QPointer<const HttpCredentials> _cred;
|
||||
};
|
||||
|
||||
HttpCredentials::HttpCredentials(const QString &accessToken)
|
||||
: _accessToken(accessToken)
|
||||
, _ready(true)
|
||||
{
|
||||
}
|
||||
|
||||
AccessManager *HttpCredentials::createAM() const
|
||||
{
|
||||
AccessManager *am = new HttpCredentialsAccessManager(this);
|
||||
|
||||
connect(am, &QNetworkAccessManager::authenticationRequired,
|
||||
this, &HttpCredentials::slotAuthentication);
|
||||
|
||||
return am;
|
||||
}
|
||||
|
||||
bool HttpCredentials::ready() const
|
||||
{
|
||||
return _ready;
|
||||
}
|
||||
|
||||
void HttpCredentials::fetchFromKeychain()
|
||||
{
|
||||
_wasFetched = true;
|
||||
|
||||
if (!_ready && !_refreshToken.isEmpty()) {
|
||||
// This happens if the credentials are still loaded from the keychain, bur we are called
|
||||
// here because the auth is invalid, so this means we simply need to refresh the credentials
|
||||
refreshAccessToken();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ready) {
|
||||
Q_EMIT fetched();
|
||||
} else {
|
||||
fetchFromKeychainHelper();
|
||||
}
|
||||
}
|
||||
|
||||
void HttpCredentials::fetchFromKeychainHelper()
|
||||
{
|
||||
auto job = _account->credentialManager()->get(refreshTokenKeyC());
|
||||
connect(job, &CredentialJob::finished, this, [job, this] {
|
||||
auto handleError = [job, this] {
|
||||
qCWarning(lcHttpCredentials) << u"Could not retrieve client password from keychain" << job->errorString();
|
||||
|
||||
// we come here if the password is empty or any other keychain
|
||||
// error happend.
|
||||
|
||||
_fetchErrorString = job->error() != QKeychain::EntryNotFound ? job->errorString() : QString();
|
||||
|
||||
_accessToken.clear();
|
||||
_ready = false;
|
||||
Q_EMIT fetched();
|
||||
};
|
||||
if (job->error() != QKeychain::NoError) {
|
||||
handleError();
|
||||
return;
|
||||
}
|
||||
const auto data = job->data().toString();
|
||||
if (OC_ENSURE(!data.isEmpty())) {
|
||||
_refreshToken = data;
|
||||
refreshAccessToken();
|
||||
} else {
|
||||
handleError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HttpCredentials::checkCredentials(QNetworkReply *reply)
|
||||
{
|
||||
// The function is called in order to determine whether we need to ask the user for a password
|
||||
// if we are using OAuth, we already started a refresh in slotAuthentication, at least in theory, ensure the auth is started.
|
||||
// If the refresh fails, we are going to Q_EMIT authenticationFailed ourselves
|
||||
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||
slotAuthentication(reply, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpCredentials::slotAuthentication(QNetworkReply *reply, QAuthenticator *authenticator)
|
||||
{
|
||||
qCDebug(lcHttpCredentials) << Q_FUNC_INFO << reply;
|
||||
if (!_ready)
|
||||
return;
|
||||
Q_UNUSED(authenticator)
|
||||
// Because of issue #4326, we need to set the login and password manually at every requests
|
||||
// Thus, if we reach this signal, those credentials were invalid and we terminate.
|
||||
qCWarning(lcHttpCredentials) << u"Stop request: Authentication failed for " << reply->url().toString() << reply->request().rawHeader("Original-Request-ID");
|
||||
reply->setProperty(authenticationFailedC, true);
|
||||
|
||||
if (!_oAuthJob) {
|
||||
qCInfo(lcHttpCredentials) << u"Refreshing token";
|
||||
refreshAccessToken();
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpCredentials::refreshAccessToken()
|
||||
{
|
||||
return refreshAccessTokenInternal(0);
|
||||
}
|
||||
|
||||
bool HttpCredentials::refreshAccessTokenInternal(int tokenRefreshRetriesCount)
|
||||
{
|
||||
if (_refreshToken.isEmpty())
|
||||
return false;
|
||||
if (_oAuthJob) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// don't touch _ready or the account state will start a new authentication
|
||||
// _ready = false;
|
||||
|
||||
// parent with nam to ensure we reset when the nam is reset
|
||||
_oAuthJob = new AccountBasedOAuth(_account->sharedFromThis(), _account->accessManager());
|
||||
connect(_oAuthJob, &AccountBasedOAuth::refreshError, this, [tokenRefreshRetriesCount, this](QNetworkReply::NetworkError error, const QString &) {
|
||||
_oAuthJob->deleteLater();
|
||||
|
||||
auto networkUnavailable = []() {
|
||||
if (auto qni = QNetworkInformation::instance()) {
|
||||
if (qni->reachability() == QNetworkInformation::Reachability::Disconnected) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
int nextTry = tokenRefreshRetriesCount + 1;
|
||||
std::chrono::seconds timeout = {};
|
||||
|
||||
if (networkUnavailable()) {
|
||||
nextTry = 0;
|
||||
timeout = TokenRefreshDefaultTimeout;
|
||||
} else {
|
||||
switch (error) {
|
||||
case QNetworkReply::ContentNotFoundError:
|
||||
// 404: bigip f5?
|
||||
timeout = 0s;
|
||||
break;
|
||||
case QNetworkReply::HostNotFoundError:
|
||||
[[fallthrough]];
|
||||
case QNetworkReply::TimeoutError:
|
||||
[[fallthrough]];
|
||||
// Qt reports OperationCanceledError if the request timed out
|
||||
case QNetworkReply::OperationCanceledError:
|
||||
[[fallthrough]];
|
||||
case QNetworkReply::TemporaryNetworkFailureError:
|
||||
[[fallthrough]];
|
||||
// VPN not ready?
|
||||
case QNetworkReply::ConnectionRefusedError:
|
||||
nextTry = 0;
|
||||
[[fallthrough]];
|
||||
default:
|
||||
timeout = TokenRefreshDefaultTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextTry >= TokenRefreshMaxRetries) {
|
||||
qCWarning(lcHttpCredentials) << u"Too many failed refreshes" << nextTry << u"-> log out";
|
||||
forgetSensitiveData();
|
||||
Q_EMIT authenticationFailed();
|
||||
Q_EMIT fetched();
|
||||
return;
|
||||
}
|
||||
QTimer::singleShot(timeout, this, [nextTry, this] {
|
||||
refreshAccessTokenInternal(nextTry);
|
||||
});
|
||||
Q_EMIT authenticationFailed();
|
||||
});
|
||||
|
||||
connect(_oAuthJob, &AccountBasedOAuth::refreshFinished, this, [this](const QString &accessToken, const QString &refreshToken) {
|
||||
_oAuthJob->deleteLater();
|
||||
if (refreshToken.isEmpty()) {
|
||||
// an error occured, log out
|
||||
forgetSensitiveData();
|
||||
Q_EMIT authenticationFailed();
|
||||
Q_EMIT fetched();
|
||||
return;
|
||||
}
|
||||
_refreshToken = refreshToken;
|
||||
if (!accessToken.isNull()) {
|
||||
_ready = true;
|
||||
_accessToken = accessToken;
|
||||
persist();
|
||||
}
|
||||
Q_EMIT fetched();
|
||||
});
|
||||
Q_EMIT authenticationStarted();
|
||||
_oAuthJob->refreshAuthentication(_refreshToken);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpCredentials::invalidateToken()
|
||||
{
|
||||
qCWarning(lcHttpCredentials) << u"Invalidating the credentials";
|
||||
|
||||
if (!_accessToken.isEmpty()) {
|
||||
_previousPassword = _accessToken;
|
||||
}
|
||||
_accessToken = QString();
|
||||
_ready = false;
|
||||
|
||||
// clear the session cookie.
|
||||
_account->clearCookieJar();
|
||||
|
||||
if (!_refreshToken.isEmpty()) {
|
||||
// Only invalidate the access_token (_password) but keep the _refreshToken in the keychain
|
||||
// (when coming from forgetSensitiveData, the _refreshToken is cleared)
|
||||
return;
|
||||
}
|
||||
|
||||
_account->credentialManager()->clear(QStringLiteral("http"));
|
||||
// let QNAM forget about the password
|
||||
// This needs to be done later in the event loop because we might be called (directly or
|
||||
// indirectly) from QNetworkAccessManagerPrivate::authenticationRequired, which itself
|
||||
// is a called from a BlockingQueuedConnection from the Qt HTTP thread. And clearing the
|
||||
// cache needs to synchronize again with the HTTP thread.
|
||||
QTimer::singleShot(0, _account, &Account::clearAMCache);
|
||||
}
|
||||
|
||||
void HttpCredentials::forgetSensitiveData()
|
||||
{
|
||||
// need to be done before invalidateToken, so it actually deletes the refresh_token from the keychain
|
||||
_refreshToken.clear();
|
||||
|
||||
invalidateToken();
|
||||
_previousPassword.clear();
|
||||
}
|
||||
|
||||
void HttpCredentials::persist()
|
||||
{
|
||||
// write secrets to the keychain
|
||||
// _refreshToken should only be empty when we are logged out...
|
||||
if (!_refreshToken.isEmpty()) {
|
||||
_account->credentialManager()->set(refreshTokenKeyC(), _refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#include "httpcredentials.moc"
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) by Klaas Freitag <freitag@kde.org>
|
||||
* Copyright (C) by Krzesimir Nowak <krzesimir@endocode.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#ifndef MIRALL_CREDS_HTTP_CREDENTIALS_H
|
||||
#define MIRALL_CREDS_HTTP_CREDENTIALS_H
|
||||
|
||||
#include "creds/abstractcredentials.h"
|
||||
#include "creds/oauth.h"
|
||||
#include "networkjobs.h"
|
||||
|
||||
#include <QSslKey>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
class QNetworkReply;
|
||||
class QAuthenticator;
|
||||
|
||||
namespace OCC {
|
||||
class OAuth;
|
||||
|
||||
/*
|
||||
The authentication system is this way because of Shibboleth.
|
||||
There used to be two different ways to authenticate: Shibboleth and HTTP Basic Auth.
|
||||
AbstractCredentials can be inherited from both ShibbolethCrendentials and HttpCredentials.
|
||||
|
||||
HttpCredentials is then split in HttpCredentials and HttpCredentialsGui.
|
||||
|
||||
This class handle both HTTP Basic Auth and OAuth. But anything that needs GUI to ask the user
|
||||
is in HttpCredentialsGui.
|
||||
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT HttpCredentials : public AbstractCredentials
|
||||
{
|
||||
Q_OBJECT
|
||||
friend class HttpCredentialsAccessManager;
|
||||
|
||||
public:
|
||||
/// Don't add credentials if this is set on a QNetworkRequest
|
||||
static constexpr QNetworkRequest::Attribute DontAddCredentialsAttribute = QNetworkRequest::User;
|
||||
|
||||
explicit HttpCredentials(const QString &accessToken);
|
||||
|
||||
AccessManager *createAM() const override;
|
||||
bool ready() const override;
|
||||
void fetchFromKeychain() override;
|
||||
void checkCredentials(QNetworkReply *reply) override;
|
||||
void persist() override;
|
||||
void invalidateToken() override;
|
||||
void forgetSensitiveData() override;
|
||||
|
||||
/* If we still have a valid refresh token, try to refresh it assynchronously and Q_EMIT fetched()
|
||||
* otherwise return false
|
||||
*/
|
||||
bool refreshAccessToken();
|
||||
|
||||
|
||||
protected:
|
||||
HttpCredentials() = default;
|
||||
|
||||
void slotAuthentication(QNetworkReply *reply, QAuthenticator *authenticator);
|
||||
void fetchFromKeychainHelper();
|
||||
|
||||
QString _accessToken;
|
||||
QString _refreshToken;
|
||||
QString _previousPassword;
|
||||
|
||||
QString _fetchErrorString;
|
||||
bool _ready = false;
|
||||
QPointer<AccountBasedOAuth> _oAuthJob;
|
||||
|
||||
private:
|
||||
bool refreshAccessTokenInternal(int tokenRefreshRetriesCount);
|
||||
};
|
||||
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hvonreth@opencloud.eu>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "idtoken.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
IdToken::IdToken() { }
|
||||
|
||||
IdToken::IdToken(const QJsonObject &payload)
|
||||
: _payload(payload)
|
||||
{
|
||||
}
|
||||
|
||||
QVariantList IdToken::aud() const
|
||||
{
|
||||
const auto aud = _payload.value("aud"_L1);
|
||||
if (aud.isArray()) {
|
||||
return aud.toArray().toVariantList();
|
||||
}
|
||||
return {aud.toString()};
|
||||
}
|
||||
|
||||
QString IdToken::sub() const
|
||||
{
|
||||
return _payload.value("sub"_L1).toString();
|
||||
}
|
||||
|
||||
QString IdToken::preferred_username() const
|
||||
{
|
||||
return _payload.value("preferred_username"_L1).toString();
|
||||
}
|
||||
|
||||
QString IdToken::name() const
|
||||
{
|
||||
return _payload.value("name"_L1).toString();
|
||||
}
|
||||
|
||||
bool IdToken::isValid() const
|
||||
{
|
||||
return !_payload.isEmpty();
|
||||
}
|
||||
|
||||
QJsonObject IdToken::toJson() const
|
||||
{
|
||||
return _payload;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hvonreth@opencloud.eu>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||
namespace OCC {
|
||||
class IdToken
|
||||
{
|
||||
public:
|
||||
IdToken();
|
||||
explicit IdToken(const QJsonObject &paylod);
|
||||
|
||||
QVariantList aud() const;
|
||||
|
||||
QString sub() const;
|
||||
|
||||
QString preferred_username() const;
|
||||
QString name() const;
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
|
||||
private:
|
||||
QJsonObject _payload;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hvonreth@opencloud.eu>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
#include "jwt.h"
|
||||
|
||||
|
||||
OCC::JWT::JWT(const QByteArray &jwt)
|
||||
{
|
||||
auto parts = jwt.split('.');
|
||||
if (parts.size() != 3) {
|
||||
return;
|
||||
}
|
||||
auto parse = [](const QByteArray &part) { return QJsonDocument::fromJson(QByteArray::fromBase64(part)).object(); };
|
||||
_header = parse(parts[0]);
|
||||
_payload = parse(parts[1]);
|
||||
_signauture = parts[2];
|
||||
}
|
||||
|
||||
QByteArray OCC::JWT::serialize() const
|
||||
{
|
||||
if (!isValid()) {
|
||||
return {};
|
||||
}
|
||||
auto encode = [](const QJsonObject &part) {
|
||||
return QJsonDocument(part).toJson(QJsonDocument::Compact).toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
};
|
||||
return encode(_header) + '.' + encode(_payload) + '.' + _signauture;
|
||||
}
|
||||
|
||||
bool OCC::JWT::isValid() const
|
||||
{
|
||||
return !_header.isEmpty() && !_payload.isEmpty() && !_signauture.isEmpty();
|
||||
}
|
||||
|
||||
QJsonObject OCC::JWT::header() const
|
||||
{
|
||||
return _header;
|
||||
}
|
||||
|
||||
QJsonObject OCC::JWT::payload() const
|
||||
{
|
||||
return _payload;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hvonreth@opencloud.eu>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
#pragma once
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
namespace OCC {
|
||||
class JWT
|
||||
{
|
||||
public:
|
||||
/*
|
||||
* Basic helper class for JWT
|
||||
* As always require https, we are not requireed to verify the signature
|
||||
*/
|
||||
explicit JWT(const QByteArray &jwt);
|
||||
|
||||
QByteArray serialize() const;
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
QJsonObject header() const;
|
||||
|
||||
QJsonObject payload() const;
|
||||
|
||||
protected:
|
||||
QJsonObject _header;
|
||||
QJsonObject _payload;
|
||||
QByteArray _signauture;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,790 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "creds/oauth.h"
|
||||
|
||||
#include "account.h"
|
||||
#include "common/version.h"
|
||||
#include "credentialmanager.h"
|
||||
#include "creds/httpcredentials.h"
|
||||
#include "networkjobs/checkserverjobfactory.h"
|
||||
#include "networkjobs/fetchuserinfojobfactory.h"
|
||||
#include "networkjobs/jsonjob.h"
|
||||
#include "resources/template.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDesktopServices>
|
||||
#include <QFile>
|
||||
#include <QIcon>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QPixmap>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
using namespace std::chrono;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcOauth, "sync.credentials.oauth", QtInfoMsg)
|
||||
|
||||
namespace {
|
||||
|
||||
const QString wellKnownPathC = QStringLiteral("/.well-known/openid-configuration");
|
||||
QString redirectUrlC()
|
||||
{
|
||||
return QStringLiteral("http://127.0.0.1");
|
||||
}
|
||||
|
||||
auto defaultOauthPromtValue()
|
||||
{
|
||||
static const auto promptValue = [] {
|
||||
OAuth::PromptValuesSupportedFlags out = OAuth::PromptValuesSupported::none;
|
||||
// convert the legacy openIdConnectPrompt() to QFlags
|
||||
for (const auto &x : Theme::instance()->openIdConnectPrompt().split(QLatin1Char(' '))) {
|
||||
out |= Utility::stringToEnum<OAuth::PromptValuesSupported>(x);
|
||||
}
|
||||
return out;
|
||||
}();
|
||||
return promptValue;
|
||||
}
|
||||
|
||||
QString renderHttpTemplate(const QString &title, const QString &content)
|
||||
{
|
||||
auto loadFile = [](const QString &font) {
|
||||
QFile f(font);
|
||||
OC_ASSERT(f.open(QFile::ReadOnly));
|
||||
return f.readAll().toBase64();
|
||||
};
|
||||
return Resources::Template::renderTemplateFromFile(QStringLiteral(":/client/resources/oauth/oauth.html.in"),
|
||||
{
|
||||
{"TITLE", title}, //
|
||||
{"CONTENT", content}, //
|
||||
{"ICON", loadFile(QStringLiteral(":/client/QSfera/theme/universal/wizard_logo.svg"))}, //
|
||||
{"BACKGROUND_COLOR", Theme::instance()->wizardHeaderBackgroundColor().name()}, //
|
||||
{"FONT_COLOR", Theme::instance()->wizardHeaderTitleColor().name()}, //
|
||||
});
|
||||
}
|
||||
|
||||
auto defaultTimeout()
|
||||
{
|
||||
// as the OAuth process can be interactive we don't want 5min of inactivity
|
||||
return qMin<milliseconds>(30s, OCC::AbstractNetworkJob::httpTimeout);
|
||||
}
|
||||
|
||||
auto defaultTimeoutMs()
|
||||
{
|
||||
return static_cast<int>(duration_cast<milliseconds>(defaultTimeout()).count());
|
||||
}
|
||||
|
||||
QString dynamicRegistrationDataC()
|
||||
{
|
||||
return QStringLiteral("oauth/dynamicRegistration");
|
||||
}
|
||||
|
||||
QString idTokenC()
|
||||
{
|
||||
return QStringLiteral("oauth/id_token");
|
||||
}
|
||||
|
||||
QVariant getRequiredField(const QVariantMap &json, const QString &s, QString *error)
|
||||
{
|
||||
const auto out = json.constFind(s);
|
||||
if (out == json.constEnd()) {
|
||||
error->append(QStringLiteral("\tError: Missing field %1\n").arg(s));
|
||||
return {};
|
||||
}
|
||||
return *out;
|
||||
}
|
||||
|
||||
void httpReplyAndClose(const QPointer<QTcpSocket> &socket, const QString &code, const QString &title, const QString &body = {}, const QStringList &additionalHeader = {})
|
||||
{
|
||||
if (!socket) {
|
||||
return; // socket can have been deleted if the browser was closed
|
||||
}
|
||||
|
||||
const QByteArray content = renderHttpTemplate(title, body.isEmpty() ? title : body).toUtf8();
|
||||
QString header = QStringLiteral("HTTP/1.1 %1\r\n"
|
||||
"Content-Type: text/html; charset=utf-8\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Content-Length: %2\r\n")
|
||||
.arg(code, QString::number(content.length()));
|
||||
|
||||
if (!additionalHeader.isEmpty()) {
|
||||
const QString nl = QStringLiteral("\r\n");
|
||||
header += additionalHeader.join(nl) + nl;
|
||||
}
|
||||
|
||||
const QByteArray msg = header.toUtf8() + QByteArrayLiteral("\r\n") + content;
|
||||
|
||||
qCDebug(lcOauth) << u"replying with HTTP response and closing socket:" << msg;
|
||||
|
||||
socket->write(msg);
|
||||
socket->disconnectFromHost();
|
||||
|
||||
// We don't want that deleting the server too early prevent queued data to be sent on this socket.
|
||||
// The socket will be deleted after disconnection because disconnected is connected to deleteLater
|
||||
socket->setParent(nullptr);
|
||||
}
|
||||
|
||||
class RegisterClientJob : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
RegisterClientJob(QNetworkAccessManager *networkAccessManager, QVariantMap &dynamicRegistrationData, const QUrl ®istrationEndpoint, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _networkAccessManager(networkAccessManager)
|
||||
, _dynamicRegistrationData(dynamicRegistrationData)
|
||||
, _registrationEndpoint(registrationEndpoint)
|
||||
{
|
||||
connect(this, &RegisterClientJob::errorOccured, this, &RegisterClientJob::deleteLater);
|
||||
connect(this, &RegisterClientJob::finished, this, &RegisterClientJob::deleteLater);
|
||||
}
|
||||
|
||||
void start()
|
||||
{
|
||||
if (!_dynamicRegistrationData.isEmpty()) {
|
||||
registerClientFinished(_dynamicRegistrationData);
|
||||
} else {
|
||||
registerClientOnline();
|
||||
}
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void finished(const QString &clientId, const QString &clientSecret, const QVariantMap &dynamicRegistrationData);
|
||||
void errorOccured(const QString &error);
|
||||
|
||||
private:
|
||||
void registerClientOnline()
|
||||
{
|
||||
const QJsonObject json(
|
||||
{{QStringLiteral("client_name"), QStringLiteral("%1 %2").arg(Theme::instance()->appNameGUI(), OCC::Version::versionWithBuildNumber().toString())},
|
||||
{QStringLiteral("redirect_uris"), QJsonArray{QStringLiteral("http://127.0.0.1")}},
|
||||
{QStringLiteral("application_type"), QStringLiteral("native")}, //
|
||||
{QStringLiteral("token_endpoint_auth_method"), QStringLiteral("none")}});
|
||||
QNetworkRequest req;
|
||||
req.setUrl(_registrationEndpoint);
|
||||
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
|
||||
req.setTransferTimeout(defaultTimeoutMs());
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/json"));
|
||||
auto reply = _networkAccessManager->post(req, QJsonDocument(json).toJson());
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, this] {
|
||||
// https://datatracker.ietf.org/doc/html/rfc7591#section-3.2
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 201) {
|
||||
const auto data = reply->readAll();
|
||||
QJsonParseError error{};
|
||||
const auto json = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error == QJsonParseError::NoError) {
|
||||
registerClientFinished(json.object().toVariantMap());
|
||||
} else {
|
||||
qCWarning(lcOauth) << u"Failed to register the client" << error.errorString() << data;
|
||||
Q_EMIT errorOccured(error.errorString());
|
||||
}
|
||||
} else {
|
||||
Q_EMIT errorOccured(reply->errorString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void registerClientFinished(const QVariantMap &data)
|
||||
{
|
||||
// extracting these values could be done by the signal receiver, too, but that'd require duplicating the error handling code
|
||||
// therefore, we extract the values here and pass them separately in the signal
|
||||
// sure, the data will be redundant, but it's worth it
|
||||
QString error;
|
||||
const auto client_id = getRequiredField(data, QStringLiteral("client_id"), &error).toString();
|
||||
if (!error.isEmpty()) {
|
||||
Q_EMIT errorOccured(error);
|
||||
return;
|
||||
}
|
||||
Q_EMIT finished(client_id, {}, data);
|
||||
}
|
||||
|
||||
private:
|
||||
QNetworkAccessManager *_networkAccessManager;
|
||||
QVariantMap _dynamicRegistrationData;
|
||||
QUrl _registrationEndpoint;
|
||||
};
|
||||
|
||||
void logCredentialsJobResult(CredentialJob *credentialsJob)
|
||||
{
|
||||
qCDebug(lcOauth) << u"credentials job has finished";
|
||||
|
||||
if (!credentialsJob->data().isValid()) {
|
||||
qCInfo(lcOauth) << u"Failed to read client id" << credentialsJob->errorString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OAuth::OAuth(const QUrl &serverUrl, QNetworkAccessManager *networkAccessManager, const QVariantMap &dynamicRegistrationData, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _serverUrl(serverUrl)
|
||||
, _dynamicRegistrationData(dynamicRegistrationData)
|
||||
, _networkAccessManager(networkAccessManager)
|
||||
, _clientId(Theme::instance()->oauthClientId())
|
||||
, _clientSecret(Theme::instance()->oauthClientSecret())
|
||||
, _supportedPromtValues(defaultOauthPromtValue())
|
||||
{
|
||||
}
|
||||
|
||||
OAuth::~OAuth() = default;
|
||||
|
||||
void OAuth::setIdToken(IdToken &&idToken)
|
||||
{
|
||||
_idToken = std::move(idToken);
|
||||
}
|
||||
|
||||
const IdToken &OAuth::idToken() const
|
||||
{
|
||||
return _idToken;
|
||||
}
|
||||
|
||||
QVariantMap OAuth::dynamicRegistrationData() const
|
||||
{
|
||||
return _dynamicRegistrationData;
|
||||
}
|
||||
|
||||
void OAuth::startAuthentication()
|
||||
{
|
||||
qCDebug(lcOauth) << u"starting authentication";
|
||||
|
||||
// Listen on the socket to get a port which will be used in the redirect_uri
|
||||
|
||||
for (const auto port : Theme::instance()->oauthPorts()) {
|
||||
if (_server.listen(QHostAddress::LocalHost, port)) {
|
||||
break;
|
||||
}
|
||||
qCDebug(lcOauth) << u"Creating local server Port:" << port << u"failed. Error:" << _server.errorString();
|
||||
}
|
||||
if (!_server.isListening()) {
|
||||
qCDebug(lcOauth) << u"server is not listening";
|
||||
Q_EMIT result(Error, {});
|
||||
return;
|
||||
}
|
||||
|
||||
_pkceCodeVerifier = generateRandomString(24);
|
||||
OC_ASSERT(_pkceCodeVerifier.size() == 128)
|
||||
_state = generateRandomString(8);
|
||||
|
||||
connect(this, &OAuth::fetchWellKnownFinished, this, [this] {
|
||||
connect(this, &AccountBasedOAuth::dynamicRegistrationDataReceived, this, &OAuth::authorisationLinkChanged);
|
||||
updateDynamicRegistration();
|
||||
});
|
||||
|
||||
fetchWellKnown();
|
||||
|
||||
QObject::connect(&_server, &QTcpServer::newConnection, this, [this] {
|
||||
while (QPointer<QTcpSocket> socket = _server.nextPendingConnection()) {
|
||||
qCDebug(lcOauth) << u"accepted client connection from" << socket->peerAddress();
|
||||
|
||||
QObject::connect(socket.data(), &QTcpSocket::disconnected, socket.data(), &QTcpSocket::deleteLater);
|
||||
|
||||
QObject::connect(socket.data(), &QIODevice::readyRead, this, [this, socket] {
|
||||
const QByteArray peek = socket->peek(qMin(socket->bytesAvailable(), 4000LL)); //The code should always be within the first 4K
|
||||
|
||||
// wait until we find a \n
|
||||
if (!peek.contains('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(lcOauth) << u"Server provided:" << peek;
|
||||
|
||||
const auto getPrefix = QByteArrayLiteral("GET /?");
|
||||
if (!peek.startsWith(getPrefix)) {
|
||||
httpReplyAndClose(socket, QStringLiteral("404 Not Found"), QStringLiteral("404 Not Found"));
|
||||
return;
|
||||
}
|
||||
const auto endOfUrl = peek.indexOf(' ', getPrefix.length());
|
||||
const QUrlQuery args(QUrl::fromPercentEncoding(peek.mid(getPrefix.length(), endOfUrl - getPrefix.length())));
|
||||
if (args.queryItemValue(QStringLiteral("state")).toUtf8() != _state) {
|
||||
httpReplyAndClose(socket, QStringLiteral("400 Bad Request"), QStringLiteral("400 Bad Request"));
|
||||
return;
|
||||
}
|
||||
|
||||
// server port cannot be queried any more after server has been closed, which we want to do as early as possible in the processing chain
|
||||
// therefore we have to store it beforehand
|
||||
const auto serverPort = _server.serverPort();
|
||||
|
||||
// we only allow one response
|
||||
qCDebug(lcOauth) << u"Received the first valid response, closing server socket";
|
||||
_server.close();
|
||||
|
||||
auto reply = postTokenRequest({
|
||||
{QStringLiteral("grant_type"), QStringLiteral("authorization_code")},
|
||||
{QStringLiteral("code"), args.queryItemValue(QStringLiteral("code"))},
|
||||
{QStringLiteral("redirect_uri"), QStringLiteral("%1:%2").arg(redirectUrlC(), QString::number(serverPort))},
|
||||
{QStringLiteral("code_verifier"), QString::fromUtf8(_pkceCodeVerifier)},
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, socket, this] {
|
||||
const auto jsonData = reply->readAll();
|
||||
QJsonParseError jsonParseError;
|
||||
const auto data = QJsonDocument::fromJson(jsonData, &jsonParseError).object().toVariantMap();
|
||||
QString fieldsError;
|
||||
const QString accessToken = getRequiredField(data, QStringLiteral("access_token"), &fieldsError).toString();
|
||||
const QString refreshToken = getRequiredField(data, QStringLiteral("refresh_token"), &fieldsError).toString();
|
||||
const QString tokenType = getRequiredField(data, QStringLiteral("token_type"), &fieldsError).toString().toLower();
|
||||
const QUrl messageUrl = QUrl::fromEncoded(data[QStringLiteral("message_url")].toByteArray());
|
||||
auto idToken = IdToken(JWT(getRequiredField(data, QStringLiteral("id_token"), &fieldsError).toByteArray()).payload());
|
||||
|
||||
|
||||
auto reportError = [socket, this](const QString &errorReason) {
|
||||
qCWarning(lcOauth) << u"Error when getting the accessToken" << errorReason;
|
||||
httpReplyAndClose(
|
||||
socket, QStringLiteral("500 Internal Server Error"), tr("Login Error"), tr("<h1>Login Error</h1><p>%1</p>").arg(errorReason));
|
||||
Q_EMIT result(Error);
|
||||
};
|
||||
if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError
|
||||
|| !fieldsError.isEmpty()
|
||||
|| tokenType != QLatin1String("bearer")) {
|
||||
// do we have error message suitable for users?
|
||||
QString errorReason = data[QStringLiteral("error_description")].toString();
|
||||
if (errorReason.isEmpty()) {
|
||||
// fall back to technical error
|
||||
errorReason = data[QStringLiteral("error")].toString();
|
||||
}
|
||||
if (!errorReason.isEmpty()) {
|
||||
reportError(tr("Error returned from the server: <em>%1</em>").arg(errorReason.toHtmlEscaped()));
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
reportError(tr("There was an error accessing the 'token' endpoint: <br><em>%1</em>").arg(reply->errorString().toHtmlEscaped()));
|
||||
} else if (jsonParseError.error != QJsonParseError::NoError) {
|
||||
reportError(tr("Could not parse the JSON returned from the server: <br><em>%1</em>").arg(jsonParseError.errorString()));
|
||||
} else if (tokenType != QStringLiteral("bearer")) {
|
||||
reportError(tr("Unsupported token type: %1").arg(tokenType));
|
||||
} else if (!fieldsError.isEmpty()) {
|
||||
reportError(tr("The reply from the server did not contain all expected fields\n:%1").arg(fieldsError));
|
||||
} else {
|
||||
reportError(tr("Unknown Error"));
|
||||
}
|
||||
} else if (!idToken.aud().contains(_clientId)) {
|
||||
reportError(tr("The audience of the id_token did not contain \"%1\"").arg(_clientId));
|
||||
} else if (_idToken.isValid() && _idToken.sub() != idToken.sub()) {
|
||||
// Connected with the wrong user
|
||||
qCWarning(lcOauth) << u"We expected the user" << _idToken.toJson() << u"but the server answered with user" << idToken.toJson();
|
||||
const QString expectedName = !_idToken.preferred_username().isEmpty() ? _idToken.preferred_username() : _idToken.name();
|
||||
const QString actualName = !idToken.preferred_username().isEmpty() ? idToken.preferred_username() : idToken.name();
|
||||
QString message;
|
||||
if (!expectedName.isEmpty() && !actualName.isEmpty() && expectedName != actualName) {
|
||||
message = tr("<h1>Incorrect user</h1>"
|
||||
"<p>You logged-in as user <em>%1</em>, but must login with user <em>%2</em>.<br>"
|
||||
"Please return to the %3 and restart the authentication.</p>")
|
||||
.arg(actualName, expectedName, Theme::instance()->appNameGUI());
|
||||
} else {
|
||||
message = tr("<h1>Incorrect user</h1>"
|
||||
"<p>You logged-in as a different user than is associated with this account.<br>"
|
||||
"Please return to the %1 and restart the authentication.</p>")
|
||||
.arg(Theme::instance()->appNameGUI());
|
||||
}
|
||||
httpReplyAndClose(socket, QStringLiteral("403 Forbidden"), tr("Incorrect user"), message);
|
||||
Q_EMIT result(Error);
|
||||
} else {
|
||||
setIdToken(std::move(idToken));
|
||||
finalize(socket, accessToken, refreshToken, messageUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void OAuth::finalize(const QPointer<QTcpSocket> &socket, const QString &accessToken, const QString &refreshToken, const QUrl &messageUrl)
|
||||
{
|
||||
const QString loginSuccessfulHtml = tr("<h1>Login successful</h1><p>You can close this window.</p>");
|
||||
const QString loginSuccessfulTitle = tr("Login successful");
|
||||
if (messageUrl.isValid()) {
|
||||
httpReplyAndClose(socket, QStringLiteral("303 See Other"), loginSuccessfulTitle, loginSuccessfulHtml,
|
||||
{QStringLiteral("Location: %1").arg(QString::fromUtf8(messageUrl.toEncoded()))});
|
||||
} else {
|
||||
httpReplyAndClose(socket, QStringLiteral("200 OK"), loginSuccessfulTitle, loginSuccessfulHtml);
|
||||
}
|
||||
Q_EMIT result(LoggedIn, accessToken, refreshToken);
|
||||
}
|
||||
|
||||
QNetworkReply *OAuth::postTokenRequest(QUrlQuery &&queryItems)
|
||||
{
|
||||
QNetworkRequest req;
|
||||
req.setTransferTimeout(defaultTimeoutMs());
|
||||
switch (_endpointAuthMethod) {
|
||||
case TokenEndpointAuthMethods::client_secret_basic:
|
||||
req.setRawHeader("Authorization", "Basic " + QStringLiteral("%1:%2").arg(_clientId, _clientSecret).toUtf8().toBase64());
|
||||
break;
|
||||
case TokenEndpointAuthMethods::client_secret_post:
|
||||
queryItems.addQueryItem(QStringLiteral("client_id"), _clientId);
|
||||
queryItems.addQueryItem(QStringLiteral("client_secret"), _clientSecret);
|
||||
break;
|
||||
case TokenEndpointAuthMethods::none:
|
||||
queryItems.addQueryItem(QStringLiteral("client_id"), _clientId);
|
||||
break;
|
||||
}
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded; charset=UTF-8"));
|
||||
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
|
||||
|
||||
queryItems.addQueryItem(QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(Theme::instance()->openIdConnectScopes())));
|
||||
req.setUrl(_tokenEndpoint);
|
||||
return _networkAccessManager->post(req, queryItems.toString(QUrl::FullyEncoded).toUtf8());
|
||||
}
|
||||
|
||||
QByteArray OAuth::generateRandomString(size_t size) const
|
||||
{
|
||||
// TODO: do we need a varaible size?
|
||||
std::vector<quint32> buffer(size, 0);
|
||||
QRandomGenerator::global()->fillRange(buffer.data(), static_cast<qsizetype>(size));
|
||||
return QByteArray(reinterpret_cast<char *>(buffer.data()), static_cast<int>(size * sizeof(quint32))).toBase64(QByteArray::Base64UrlEncoding);
|
||||
}
|
||||
|
||||
QUrl OAuth::authorisationLink() const
|
||||
{
|
||||
Q_ASSERT(_server.isListening());
|
||||
Q_ASSERT(_wellKnownFinished);
|
||||
Q_ASSERT(_authEndpoint.isValid());
|
||||
|
||||
const QByteArray code_challenge = QCryptographicHash::hash(_pkceCodeVerifier, QCryptographicHash::Sha256)
|
||||
.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
QUrlQuery query{
|
||||
{QStringLiteral("response_type"), QStringLiteral("code")},
|
||||
{QStringLiteral("client_id"), _clientId},
|
||||
{QStringLiteral("redirect_uri"), QStringLiteral("%1:%2").arg(redirectUrlC(), QString::number(_server.serverPort()))},
|
||||
{QStringLiteral("code_challenge"), QString::fromLatin1(code_challenge)},
|
||||
{QStringLiteral("code_challenge_method"), QStringLiteral("S256")},
|
||||
{QStringLiteral("scope"), QString::fromUtf8(QUrl::toPercentEncoding(Theme::instance()->openIdConnectScopes()))},
|
||||
{QStringLiteral("prompt"), QString::fromUtf8(QUrl::toPercentEncoding(toString(_supportedPromtValues)))},
|
||||
{QStringLiteral("state"), QString::fromUtf8(_state)},
|
||||
};
|
||||
|
||||
if (!_idToken.preferred_username().isEmpty()) {
|
||||
query.addQueryItem(QStringLiteral("login_hint"), QString::fromUtf8(QUrl::toPercentEncoding(_idToken.preferred_username())));
|
||||
}
|
||||
return Utility::concatUrlPath(_authEndpoint, {}, query);
|
||||
}
|
||||
|
||||
QString OAuth::clientId() const
|
||||
{
|
||||
return _clientId;
|
||||
}
|
||||
|
||||
QString OAuth::clientSecret() const
|
||||
{
|
||||
return _clientSecret;
|
||||
}
|
||||
|
||||
void OAuth::persist(const OCC::AccountPtr &accountPtr, const QVariantMap &dynamicRegistrationData, const IdToken &idToken)
|
||||
{
|
||||
if (!dynamicRegistrationData.isEmpty()) {
|
||||
accountPtr->credentialManager()->set(dynamicRegistrationDataC(), dynamicRegistrationData);
|
||||
} else {
|
||||
accountPtr->credentialManager()->clear(dynamicRegistrationDataC());
|
||||
}
|
||||
if (idToken.isValid()) {
|
||||
accountPtr->credentialManager()->set(idTokenC(), idToken.toJson());
|
||||
} else {
|
||||
accountPtr->credentialManager()->clear(idTokenC());
|
||||
}
|
||||
}
|
||||
|
||||
void OAuth::updateDynamicRegistration()
|
||||
{
|
||||
// this slightly complicated construct allows us to log case-specific messages
|
||||
if (!Theme::instance()->oidcEnableDynamicRegistration()) {
|
||||
qCDebug(lcOauth) << u"dynamic registration disabled by theme";
|
||||
} else if (!_registrationEndpoint.isValid()) {
|
||||
qCDebug(lcOauth) << u"registration endpoint not provided or empty:" << _registrationEndpoint.toString()
|
||||
<< u"we asume dynamic registration is not supported by the server";
|
||||
} else {
|
||||
auto registerJob = new RegisterClientJob(_networkAccessManager, _dynamicRegistrationData, _registrationEndpoint, this);
|
||||
connect(registerJob, &RegisterClientJob::finished, this,
|
||||
[this](const QString &clientId, const QString &clientSecret, const QVariantMap &dynamicRegistrationData) {
|
||||
qCDebug(lcOauth) << u"client registration finished successfully";
|
||||
_clientId = clientId;
|
||||
_clientSecret = clientSecret;
|
||||
_dynamicRegistrationData = dynamicRegistrationData;
|
||||
Q_EMIT dynamicRegistrationDataReceived();
|
||||
});
|
||||
connect(registerJob, &RegisterClientJob::errorOccured, this, [this](const QString &error) {
|
||||
qCWarning(lcOauth) << u"Failed to dynamically register the client, try the default client id" << error;
|
||||
Q_EMIT dynamicRegistrationDataReceived();
|
||||
});
|
||||
registerJob->start();
|
||||
return;
|
||||
}
|
||||
Q_EMIT dynamicRegistrationDataReceived();
|
||||
}
|
||||
|
||||
void OAuth::fetchWellKnown()
|
||||
{
|
||||
const QPair<QString, QString> urls = Theme::instance()->oauthOverrideAuthUrl();
|
||||
|
||||
if (!urls.first.isNull()) {
|
||||
OC_ASSERT(!urls.second.isNull());
|
||||
_authEndpoint = QUrl(urls.first);
|
||||
_tokenEndpoint = QUrl(urls.second);
|
||||
|
||||
qCDebug(lcOauth) << u"override URL set, using auth endpoint" << _authEndpoint << u"and token endpoint" << _tokenEndpoint;
|
||||
|
||||
_wellKnownFinished = true;
|
||||
Q_EMIT fetchWellKnownFinished();
|
||||
} else {
|
||||
qCDebug(lcOauth) << u"fetching" << wellKnownPathC;
|
||||
|
||||
QNetworkRequest req;
|
||||
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
|
||||
req.setUrl(Utility::concatUrlPath(_serverUrl, wellKnownPathC));
|
||||
req.setTransferTimeout(defaultTimeoutMs());
|
||||
|
||||
auto reply = _networkAccessManager->get(req);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, this] {
|
||||
_wellKnownFinished = true;
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qCDebug(lcOauth) << u"failed to fetch .well-known reply, error:" << reply->error();
|
||||
if (_isRefreshingToken) {
|
||||
Q_EMIT refreshError(reply->error(), reply->errorString());
|
||||
} else {
|
||||
Q_EMIT result(Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
QJsonParseError err = {};
|
||||
QJsonObject data = QJsonDocument::fromJson(reply->readAll(), &err).object();
|
||||
if (err.error == QJsonParseError::NoError) {
|
||||
_authEndpoint = QUrl::fromEncoded(data[QStringLiteral("authorization_endpoint")].toString().toUtf8());
|
||||
_tokenEndpoint = QUrl::fromEncoded(data[QStringLiteral("token_endpoint")].toString().toUtf8());
|
||||
_registrationEndpoint = QUrl::fromEncoded(data[QStringLiteral("registration_endpoint")].toString().toUtf8());
|
||||
|
||||
if (_clientSecret.isEmpty()) {
|
||||
_endpointAuthMethod = TokenEndpointAuthMethods::none;
|
||||
} else {
|
||||
const auto authMethods = data.value(QStringLiteral("token_endpoint_auth_methods_supported")).toArray();
|
||||
if (authMethods.contains(QStringLiteral("none"))) {
|
||||
_endpointAuthMethod = TokenEndpointAuthMethods::none;
|
||||
} else if (authMethods.contains(QStringLiteral("client_secret_post"))) {
|
||||
_endpointAuthMethod = TokenEndpointAuthMethods::client_secret_post;
|
||||
} else if (authMethods.contains(QStringLiteral("client_secret_basic"))) {
|
||||
_endpointAuthMethod = TokenEndpointAuthMethods::client_secret_basic;
|
||||
} else {
|
||||
OC_ASSERT_X(
|
||||
false, qPrintable(QStringLiteral("Unsupported token_endpoint_auth_methods_supported: %1").arg(QDebug::toString(authMethods))));
|
||||
}
|
||||
}
|
||||
const auto promtValuesSupported = data.value(QStringLiteral("prompt_values_supported")).toArray();
|
||||
if (!promtValuesSupported.isEmpty()) {
|
||||
_supportedPromtValues = PromptValuesSupported::none;
|
||||
for (const auto &x : promtValuesSupported) {
|
||||
const auto flag = Utility::stringToEnum<PromptValuesSupported>(x.toString());
|
||||
// only use flags present in Theme::instance()->openIdConnectPrompt()
|
||||
if (flag & defaultOauthPromtValue())
|
||||
_supportedPromtValues |= flag;
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(lcOauth) << u"parsing .well-known reply successful, auth endpoint" << _authEndpoint << u"and token endpoint" << _tokenEndpoint
|
||||
<< u"and registration endpoint" << _registrationEndpoint;
|
||||
} else if (err.error == QJsonParseError::IllegalValue) {
|
||||
qCDebug(lcOauth) << u"failed to parse .well-known reply as JSON, server might not support OIDC";
|
||||
} else {
|
||||
qCDebug(lcOauth) << u"failed to parse .well-known reply, error:" << err.error;
|
||||
Q_EMIT result(Error);
|
||||
}
|
||||
Q_EMIT fetchWellKnownFinished();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a URL returned by the server is valid.
|
||||
* @param url URL to validate
|
||||
* @return true if validation is successful, false otherwise
|
||||
*/
|
||||
bool isUrlValid(const QUrl &url)
|
||||
{
|
||||
qCDebug(lcOauth()) << u"Checking URL for validity:" << url;
|
||||
|
||||
// we have hardcoded the oauthOverrideAuth
|
||||
const auto overrideUrl = Theme::instance()->oauthOverrideAuthUrl();
|
||||
if (!overrideUrl.first.isEmpty()) {
|
||||
return QUrl::fromUserInput(overrideUrl.first).matches(url, QUrl::RemoveQuery);
|
||||
}
|
||||
|
||||
// the following allowlist contains URL schemes accepted as valid
|
||||
// OAuth 2.0 URLs must be HTTPS to be in compliance with the specification
|
||||
// for unit tests, we also permit the nonexisting oauthtest scheme
|
||||
const QStringList allowedSchemes({ QStringLiteral("https"), QStringLiteral("oauthtest") });
|
||||
return allowedSchemes.contains(url.scheme());
|
||||
}
|
||||
|
||||
void OAuth::openBrowser()
|
||||
{
|
||||
Q_ASSERT(!authorisationLink().isEmpty());
|
||||
|
||||
qCDebug(lcOauth) << u"opening browser";
|
||||
|
||||
if (!isUrlValid(authorisationLink())) {
|
||||
qCWarning(lcOauth) << u"URL validation failed";
|
||||
Q_EMIT result(ErrorInsecureUrl, QString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QDesktopServices::openUrl(authorisationLink())) {
|
||||
qCWarning(lcOauth) << u"QDesktopServices::openUrl Failed";
|
||||
Q_EMIT result(Error, {});
|
||||
}
|
||||
}
|
||||
|
||||
AccountBasedOAuth::AccountBasedOAuth(AccountPtr account, QObject *parent)
|
||||
: OAuth(account->url(), account->accessManager(), {}, parent)
|
||||
, _account(account)
|
||||
{
|
||||
connect(this, &AccountBasedOAuth::result, this, [account, this](OAuth::Result result, const QString &, const QString &) {
|
||||
if (result == OAuth::LoggedIn) {
|
||||
persist(account, dynamicRegistrationData(), idToken());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void AccountBasedOAuth::startAuthentication()
|
||||
{
|
||||
qCDebug(lcOauth) << u"fetching dynamic registration data";
|
||||
connect(this, &AccountBasedOAuth::restored, this, [this] {
|
||||
// explicitly call base implementation, this can't be done directly in the connect
|
||||
OAuth::startAuthentication();
|
||||
});
|
||||
restore();
|
||||
}
|
||||
|
||||
void AccountBasedOAuth::fetchWellKnown()
|
||||
{
|
||||
qCDebug(lcOauth) << u"starting CheckServerJob before fetching" << wellKnownPathC;
|
||||
|
||||
auto *checkServerJob = CheckServerJobFactory::createFromAccount(_account, true, this).startJob(_serverUrl, this);
|
||||
|
||||
connect(checkServerJob, &CoreJob::finished, this, [checkServerJob, this]() {
|
||||
if (checkServerJob->success()) {
|
||||
qCDebug(lcOauth) << u"CheckServerJob succeeded, fetching" << wellKnownPathC;
|
||||
OAuth::fetchWellKnown();
|
||||
} else {
|
||||
qCDebug(lcOauth) << u"CheckServerJob failed, error:" << checkServerJob->errorMessage();
|
||||
if (_isRefreshingToken) {
|
||||
Q_EMIT refreshError(checkServerJob->reply()->error(), checkServerJob->errorMessage());
|
||||
} else {
|
||||
Q_EMIT result(Error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
void AccountBasedOAuth::restore()
|
||||
{
|
||||
if (_restored) {
|
||||
Q_EMIT restored(QPrivateSignal());
|
||||
return;
|
||||
}
|
||||
_restored = true;
|
||||
auto idTokenJob = _account->credentialManager()->get(idTokenC());
|
||||
connect(idTokenJob, &CredentialJob::finished, this, [idTokenJob, this] {
|
||||
if (idTokenJob->error() == QKeychain::EntryNotFound) {
|
||||
qCWarning(lcOauth) << u"idToken token token credential not found";
|
||||
} else if (idTokenJob->error() != QKeychain::NoError) {
|
||||
Q_EMIT result(Error);
|
||||
return;
|
||||
} else {
|
||||
setIdToken(IdToken(idTokenJob->data().value<QJsonObject>()));
|
||||
}
|
||||
|
||||
auto credentialsJob = _account->credentialManager()->get(dynamicRegistrationDataC());
|
||||
connect(credentialsJob, &CredentialJob::finished, this, [this, credentialsJob] {
|
||||
qCDebug(lcOauth) << u"fetched dynamic registration data" << credentialsJob->errorString();
|
||||
logCredentialsJobResult(credentialsJob);
|
||||
|
||||
_dynamicRegistrationData = credentialsJob->data().value<QVariantMap>();
|
||||
Q_EMIT restored(QPrivateSignal());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void AccountBasedOAuth::refreshAuthentication(const QString &refreshToken)
|
||||
{
|
||||
if (!OC_ENSURE(!_isRefreshingToken)) {
|
||||
qCDebug(lcOauth) << u"already refreshing token, aborting";
|
||||
return;
|
||||
}
|
||||
|
||||
_isRefreshingToken = true;
|
||||
|
||||
connect(this, &AccountBasedOAuth::restored, this, [refreshToken, this] {
|
||||
connect(this, &OAuth::fetchWellKnownFinished, this, [refreshToken, this] {
|
||||
connect(this, &AccountBasedOAuth::dynamicRegistrationDataReceived, this, [refreshToken, this] {
|
||||
auto reply =
|
||||
postTokenRequest({{QStringLiteral("grant_type"), QStringLiteral("refresh_token")}, {QStringLiteral("refresh_token"), refreshToken}});
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, refreshToken, this]() {
|
||||
const auto jsonData = reply->readAll();
|
||||
QJsonParseError jsonParseError;
|
||||
const auto data = QJsonDocument::fromJson(jsonData, &jsonParseError).object().toVariantMap();
|
||||
QString accessToken;
|
||||
QString newRefreshToken = refreshToken;
|
||||
// https://developer.okta.com/docs/reference/api/oidc/#response-properties-2
|
||||
const QString errorString = data.value(QStringLiteral("error")).toString();
|
||||
if (!errorString.isEmpty()) {
|
||||
if (errorString == QLatin1String("invalid_grant") || errorString == QLatin1String("invalid_request")) {
|
||||
newRefreshToken.clear();
|
||||
} else {
|
||||
qCWarning(lcOauth) << u"Error while refreshing the token:" << errorString
|
||||
<< data.value(QStringLiteral("error_description")).toString();
|
||||
Q_EMIT refreshError(QNetworkReply::NoError, data.value(QStringLiteral("error_description")).toString());
|
||||
return;
|
||||
}
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
qCWarning(lcOauth) << u"Error while refreshing the token:" << reply->error() << u":" << reply->errorString()
|
||||
<< reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
Q_EMIT refreshError(reply->error(), reply->errorString());
|
||||
return;
|
||||
} else {
|
||||
if (jsonParseError.error != QJsonParseError::NoError || data.isEmpty()) {
|
||||
// Invalid or empty JSON: Network error maybe?
|
||||
qCWarning(lcOauth) << u"Error while refreshing the token:" << jsonParseError.errorString();
|
||||
} else {
|
||||
QString error;
|
||||
accessToken = getRequiredField(data, QStringLiteral("access_token"), &error).toString();
|
||||
if (!error.isEmpty()) {
|
||||
qCWarning(lcOauth) << u"The reply from the server did not contain all expected fields:" << error;
|
||||
}
|
||||
|
||||
const auto refresh_token = data.find(QStringLiteral("refresh_token"));
|
||||
if (refresh_token != data.constEnd()) {
|
||||
newRefreshToken = refresh_token.value().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
Q_EMIT refreshFinished(accessToken, newRefreshToken);
|
||||
});
|
||||
});
|
||||
updateDynamicRegistration();
|
||||
});
|
||||
fetchWellKnown();
|
||||
});
|
||||
restore();
|
||||
}
|
||||
|
||||
QString OCC::toString(OAuth::PromptValuesSupportedFlags s)
|
||||
{
|
||||
QStringList out;
|
||||
for (auto k : {OAuth::PromptValuesSupported::consent, OAuth::PromptValuesSupported::select_account})
|
||||
if (s & k) {
|
||||
out += Utility::enumToString(k);
|
||||
}
|
||||
return out.join(QLatin1Char(' '));
|
||||
}
|
||||
|
||||
|
||||
#include "oauth.moc"
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "accountfwd.h"
|
||||
#include "libsync/creds/idtoken.h"
|
||||
#include "libsync/creds/jwt.h"
|
||||
#include "qsferasynclib.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QPointer>
|
||||
#include <QTcpServer>
|
||||
#include <QUrl>
|
||||
|
||||
namespace OCC {
|
||||
class JsonJob;
|
||||
|
||||
/**
|
||||
* Job that do the authorization grant and fetch the access token
|
||||
*
|
||||
* Normal workflow:
|
||||
*
|
||||
* --> start()
|
||||
* |
|
||||
* +----> fetchWellKnown() query the ".well-known/openid-configuration" endpoint
|
||||
* |
|
||||
* +----> openBrowser() open the browser after fetchWellKnown finished to the specified page
|
||||
* | (or the default 'oauth2/authorize' if fetchWellKnown does not exist)
|
||||
* | Then the browser will redirect to http://127.0.0.1:xxx
|
||||
* |
|
||||
* +----> _server starts listening on a TCP port waiting for an HTTP request with a 'code'
|
||||
* |
|
||||
* v
|
||||
* request the access_token and the refresh_token via 'apps/oauth2/api/v1/token'
|
||||
* |
|
||||
* +-> Request the user_id is not present
|
||||
* | |
|
||||
* v v
|
||||
* finalize(...): Q_EMIT result(...)
|
||||
*
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT OAuth : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Result { LoggedIn, Error, ErrorInsecureUrl };
|
||||
Q_ENUM(Result)
|
||||
enum class TokenEndpointAuthMethods : char { none, client_secret_basic, client_secret_post };
|
||||
Q_ENUM(TokenEndpointAuthMethods)
|
||||
|
||||
enum class PromptValuesSupported : char { none = 0, consent = 1 << 0, select_account = 1 << 1, login = 1 << 2 };
|
||||
Q_ENUM(PromptValuesSupported)
|
||||
Q_DECLARE_FLAGS(PromptValuesSupportedFlags, PromptValuesSupported)
|
||||
|
||||
OAuth(const QUrl &serverUrl, QNetworkAccessManager *networkAccessManager, const QVariantMap &dynamicRegistrationData, QObject *parent);
|
||||
~OAuth() override;
|
||||
|
||||
void setIdToken(IdToken &&idToken);
|
||||
const IdToken &idToken() const;
|
||||
|
||||
QVariantMap dynamicRegistrationData() const;
|
||||
|
||||
virtual void startAuthentication();
|
||||
void openBrowser();
|
||||
QUrl authorisationLink() const;
|
||||
|
||||
// TODO: private api for tests
|
||||
QString clientId() const;
|
||||
QString clientSecret() const;
|
||||
|
||||
static void persist(const AccountPtr &accountPtr, const QVariantMap &dynamicRegistrationData, const IdToken &idToken);
|
||||
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* The state has changed.
|
||||
* when logged in, token has the value of the token.
|
||||
*/
|
||||
void result(OAuth::Result result, const QString &token = QString(), const QString &refreshToken = QString());
|
||||
|
||||
/**
|
||||
* emitted when the call to the well-known endpoint is finished
|
||||
*/
|
||||
void authorisationLinkChanged();
|
||||
|
||||
void fetchWellKnownFinished();
|
||||
|
||||
void dynamicRegistrationDataReceived();
|
||||
|
||||
void refreshError(QNetworkReply::NetworkError error, const QString &errorString);
|
||||
|
||||
|
||||
protected:
|
||||
void updateDynamicRegistration();
|
||||
|
||||
QUrl _serverUrl;
|
||||
QVariantMap _dynamicRegistrationData;
|
||||
QNetworkAccessManager *_networkAccessManager;
|
||||
bool _isRefreshingToken = false;
|
||||
|
||||
QString _clientId;
|
||||
QString _clientSecret;
|
||||
|
||||
QUrl _registrationEndpoint;
|
||||
|
||||
virtual void fetchWellKnown();
|
||||
|
||||
QNetworkReply *postTokenRequest(QUrlQuery &&queryItems);
|
||||
|
||||
|
||||
private:
|
||||
void finalize(const QPointer<QTcpSocket> &socket, const QString &accessToken, const QString &refreshToken, const QUrl &messageUrl);
|
||||
|
||||
QByteArray generateRandomString(size_t size) const;
|
||||
|
||||
QTcpServer _server;
|
||||
bool _wellKnownFinished = false;
|
||||
|
||||
QUrl _authEndpoint;
|
||||
QUrl _tokenEndpoint;
|
||||
QByteArray _pkceCodeVerifier;
|
||||
QByteArray _state;
|
||||
|
||||
IdToken _idToken;
|
||||
|
||||
TokenEndpointAuthMethods _endpointAuthMethod = TokenEndpointAuthMethods::client_secret_basic;
|
||||
PromptValuesSupportedFlags _supportedPromtValues = {PromptValuesSupported::consent, PromptValuesSupported::select_account};
|
||||
};
|
||||
|
||||
/**
|
||||
* This variant of OAuth uses an account's network access manager etc.
|
||||
* Instead of relying on the user to provide a working server URL, a CheckServerJob is run upon start(), which also stores the fetched cookies in the account's state.
|
||||
* Furthermore, it takes care of storing and loading the dynamic registration data in the account's credentials manager.
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT AccountBasedOAuth : public OAuth
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AccountBasedOAuth(AccountPtr account, QObject *parent = nullptr);
|
||||
|
||||
void startAuthentication() override;
|
||||
|
||||
void refreshAuthentication(const QString &refreshToken);
|
||||
|
||||
Q_SIGNALS:
|
||||
void refreshFinished(const QString &accessToken, const QString &refreshToken);
|
||||
void restored(QPrivateSignal);
|
||||
|
||||
|
||||
protected:
|
||||
void fetchWellKnown() override;
|
||||
|
||||
|
||||
void restore();
|
||||
|
||||
private:
|
||||
AccountPtr _account;
|
||||
bool _restored = false;
|
||||
};
|
||||
|
||||
QString QSFERA_SYNC_EXPORT toString(OAuth::PromptValuesSupportedFlags s);
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(OAuth::PromptValuesSupportedFlags)
|
||||
} // namespce OCC
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "webfinger.h"
|
||||
|
||||
#include "common/asserts.h"
|
||||
|
||||
#include "httpcredentials.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLoggingCategory>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
|
||||
Q_LOGGING_CATEGORY(lcWebFinger, "sync.credentials.webfinger", QtInfoMsg)
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
WebFinger::WebFinger(QNetworkAccessManager *nam, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _nam(nam)
|
||||
{
|
||||
}
|
||||
|
||||
void WebFinger::start(const QUrl &url, const QString &resourceId)
|
||||
{
|
||||
// GET /.well-known/webfinger?rel=http://webfinger.opencloud/rel/server-instance&resource=acct:test@opencloud.eu HTTP/1.1
|
||||
if (OC_ENSURE(url.scheme() == QLatin1String("https"))) {
|
||||
QUrlQuery query;
|
||||
query.setQueryItems({ { QStringLiteral("resource"), QString::fromUtf8(QUrl::toPercentEncoding(resourceId)) },
|
||||
{ QStringLiteral("rel"), relId() } });
|
||||
|
||||
QNetworkRequest req;
|
||||
req.setUrl(Utility::concatUrlPath(url, QStringLiteral(".well-known/webfinger"), query));
|
||||
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
|
||||
|
||||
auto *reply = _nam->get(req);
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, this] {
|
||||
const auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (status == 200) {
|
||||
const auto data = reply->readAll();
|
||||
auto obj = QJsonDocument::fromJson(data, &_error).object();
|
||||
if (_error.error == QJsonParseError::NoError) {
|
||||
const auto links = obj.value(QLatin1String("links")).toArray();
|
||||
if (!links.empty()) {
|
||||
_href = QUrl::fromEncoded(links.first().toObject().value(QLatin1String("href")).toString().toUtf8());
|
||||
qCInfo(lcWebFinger) << u"Webfinger provided" << _href << u"as server";
|
||||
} else {
|
||||
qCWarning(lcWebFinger) << reply->url() << u"Did not reply a valid link";
|
||||
}
|
||||
} else {
|
||||
qCWarning(lcWebFinger) << u"Failed with" << _error.errorString();
|
||||
}
|
||||
} else {
|
||||
qCWarning(lcWebFinger) << u"Failed with status code" << status;
|
||||
_error.error = QJsonParseError::MissingObject;
|
||||
}
|
||||
Q_EMIT finished();
|
||||
});
|
||||
} else {
|
||||
Q_EMIT finished();
|
||||
}
|
||||
}
|
||||
|
||||
const QJsonParseError &WebFinger::error() const
|
||||
{
|
||||
return _error;
|
||||
}
|
||||
|
||||
const QUrl &WebFinger::href() const
|
||||
{
|
||||
return _href;
|
||||
}
|
||||
|
||||
QString WebFinger::relId()
|
||||
{
|
||||
return QStringLiteral("http://webfinger.opencloud/rel/server-instance");
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "qsferasynclib.h"
|
||||
|
||||
#include "account.h"
|
||||
|
||||
#include <QJsonParseError>
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class QSFERA_SYNC_EXPORT WebFinger : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
WebFinger(QNetworkAccessManager *nam, QObject *parent = nullptr);
|
||||
|
||||
void start(const QUrl &url, const QString &resourceId);
|
||||
|
||||
const QJsonParseError &error() const;
|
||||
|
||||
const QUrl &href() const;
|
||||
|
||||
/***
|
||||
* ID used to describe our rel attribute
|
||||
* Defined on the server in services/webfinger/pkg/relations/owncloud_instance.go as OpenCloudInstanceRel
|
||||
* The server uses "http://webfinger.opencloud/rel/server-instance" (without .eu)
|
||||
*
|
||||
* According to RFC 7033 (WebFinger), the "rel" is either a URI or a registered relation type
|
||||
* as specified in RFC 5988 (Web Linking). For custom relation types like this one,
|
||||
* a URI format is used as described in RFC 7033 section 10.3:
|
||||
* https://datatracker.ietf.org/doc/html/rfc7033#section-10.3
|
||||
*/
|
||||
static QString relId();
|
||||
|
||||
Q_SIGNALS:
|
||||
void finished();
|
||||
|
||||
private:
|
||||
QNetworkAccessManager *_nam;
|
||||
QJsonParseError _error;
|
||||
QUrl _href;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* libcsync -- a library to sync a directory with another
|
||||
*
|
||||
* Copyright (c) by Hannah von Reth <hannah.vonreth@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 "csync.h"
|
||||
|
||||
#include <QMetaEnum>
|
||||
|
||||
QDebug operator<<(QDebug debug, const SyncInstructions &enumValue)
|
||||
{
|
||||
static const QMetaEnum me = QMetaEnum::fromType<SyncInstruction>();
|
||||
QDebugStateSaver saver(debug);
|
||||
debug.nospace().noquote() << me.enumName() << u"(" << me.valueToKeys(enumValue) << u")";
|
||||
return debug;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* libcsync -- a library to sync a directory with another
|
||||
*
|
||||
* Copyright (c) 2008-2013 by Andreas Schneider <asn@cryptomilk.org>
|
||||
* Copyright (c) 2012-2013 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file csync.h
|
||||
*
|
||||
* @brief Application developer interface for csync.
|
||||
*
|
||||
* @defgroup csyncPublicAPI csync public API
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#ifndef _CSYNC_H
|
||||
#define _CSYNC_H
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
|
||||
namespace OCC {
|
||||
class SyncJournalFileRecord;
|
||||
}
|
||||
|
||||
namespace CSyncEnums {
|
||||
QSFERA_SYNC_EXPORT Q_NAMESPACE
|
||||
|
||||
|
||||
/**
|
||||
* Instruction enum. In the file traversal structure, it describes
|
||||
* the csync state of a file.
|
||||
*/
|
||||
// clang-format off
|
||||
enum SyncInstruction : uint16_t {
|
||||
CSYNC_INSTRUCTION_NONE = 1 << 1, /* Nothing to do (UPDATE|RECONCILE) */
|
||||
CSYNC_INSTRUCTION_REMOVE = 1 << 2, /* The file need to be removed (RECONCILE) */
|
||||
CSYNC_INSTRUCTION_RENAME = 1 << 3, /* The file need to be renamed (RECONCILE) */
|
||||
CSYNC_INSTRUCTION_NEW = 1 << 4, /* The file is new compared to the db (UPDATE) */
|
||||
CSYNC_INSTRUCTION_CONFLICT = 1 << 5, /* The file need to be downloaded because it is a conflict (RECONCILE) */
|
||||
CSYNC_INSTRUCTION_IGNORE = 1 << 6, /* The file is ignored (UPDATE|RECONCILE)
|
||||
* Identical to CSYNC_INSTRUCTION_NONE but logged to the user.
|
||||
*/
|
||||
CSYNC_INSTRUCTION_SYNC = 1 << 7, /* The file need to be pushed to the other remote (RECONCILE) */
|
||||
CSYNC_INSTRUCTION_ERROR = 1 << 8,
|
||||
CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
|
||||
Used when the type of something changes from directory to file
|
||||
or back. */
|
||||
CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
|
||||
but without any propagation (UPDATE|RECONCILE) */
|
||||
};
|
||||
// clang-format on
|
||||
Q_FLAG_NS(SyncInstruction)
|
||||
Q_DECLARE_FLAGS(SyncInstructions, SyncInstruction)
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(SyncInstructions)
|
||||
|
||||
// Also, this value is stored in the database, so beware of value changes.
|
||||
enum ItemType : uint8_t {
|
||||
ItemTypeFile = 0,
|
||||
ItemTypeSymLink = 1,
|
||||
ItemTypeDirectory = 2,
|
||||
ItemTypeUnsupported = 3,
|
||||
|
||||
/** The file is a dehydrated placeholder, meaning data isn't available locally */
|
||||
ItemTypeVirtualFile = 4,
|
||||
|
||||
/** A ItemTypeVirtualFile that wants to be hydrated.
|
||||
*
|
||||
* Actions may put this in the db as a request to a future sync, such as
|
||||
* implicit hydration (when the user wants to access file data) when using
|
||||
* suffix vfs. For pin-state driven hydrations changing the database is
|
||||
* not necessary.
|
||||
*
|
||||
* For some vfs plugins the placeholder files on disk may be marked for
|
||||
* (de-)hydration (like with a file attribute) and then the local discovery
|
||||
* will return this item type.
|
||||
*
|
||||
* The discovery will also use this item type to mark entries for hydration
|
||||
* if an item's pin state mandates it, such as when encountering a AlwaysLocal
|
||||
* file that is dehydrated.
|
||||
*/
|
||||
ItemTypeVirtualFileDownload = 5,
|
||||
|
||||
/** A ItemTypeFile that wants to be dehydrated.
|
||||
*
|
||||
* Similar to ItemTypeVirtualFileDownload, but there's currently no situation
|
||||
* where it's stored in the database since there is no action that triggers a
|
||||
* file dehydration without changing the pin state.
|
||||
*/
|
||||
ItemTypeVirtualFileDehydration = 6,
|
||||
};
|
||||
Q_ENUM_NS(ItemType)
|
||||
}
|
||||
|
||||
using namespace CSyncEnums;
|
||||
|
||||
QSFERA_SYNC_EXPORT QDebug operator<<(QDebug debug, const SyncInstructions &job);
|
||||
|
||||
/**
|
||||
* }@
|
||||
*/
|
||||
#endif /* _CSYNC_H */
|
||||
/* vim: set ft=c.doxygen ts=8 sw=2 et cindent: */
|
||||
@@ -0,0 +1,782 @@
|
||||
/*
|
||||
* libcsync -- a library to sync a directory with another
|
||||
*
|
||||
* Copyright (c) 2008-2013 by Andreas Schneider <asn@cryptomilk.org>
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
#include "libsync/csync_exclude.h"
|
||||
|
||||
#include "common/filesystembase.h"
|
||||
#include "common/utility.h"
|
||||
#include "common/version.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QString>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
namespace {
|
||||
|
||||
// See http://support.microsoft.com/kb/74496 and
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
|
||||
// Additionally, we ignore '$Recycle.Bin', see https://github.com/owncloud/client/issues/2955
|
||||
const QLatin1String win_device_names[] = {QLatin1String("CON"), QLatin1String("PRN"), QLatin1String("AUX"), QLatin1String("NUL"), QLatin1String("COM1"),
|
||||
QLatin1String("COM2"), QLatin1String("COM3"), QLatin1String("COM4"), QLatin1String("COM5"), QLatin1String("COM6"), QLatin1String("COM7"),
|
||||
QLatin1String("COM8"), QLatin1String("COM9"), QLatin1String("LPT1"), QLatin1String("LPT2"), QLatin1String("LPT3"), QLatin1String("LPT4"),
|
||||
QLatin1String("LPT5"), QLatin1String("LPT6"), QLatin1String("LPT7"), QLatin1String("LPT8"), QLatin1String("LPT9"), QLatin1String("CLOCK$")};
|
||||
|
||||
const QLatin1String win_system_files[] = {QLatin1String("$Recycle.Bin"), QLatin1String("System Volume Information")};
|
||||
}
|
||||
|
||||
/** Expands C-like escape sequences (in place)
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT void csync_exclude_expand_escapes(QByteArray &input)
|
||||
{
|
||||
qsizetype o = 0;
|
||||
char *line = input.data();
|
||||
auto len = input.size();
|
||||
for (int i = 0; i < len; ++i) {
|
||||
if (line[i] == '\\') {
|
||||
// at worst input[i+1] is \0
|
||||
switch (line[i + 1]) {
|
||||
case '\'':
|
||||
line[o++] = '\'';
|
||||
break;
|
||||
case '"':
|
||||
line[o++] = '"';
|
||||
break;
|
||||
case '?':
|
||||
line[o++] = '?';
|
||||
break;
|
||||
case '#':
|
||||
line[o++] = '#';
|
||||
break;
|
||||
case 'a':
|
||||
line[o++] = '\a';
|
||||
break;
|
||||
case 'b':
|
||||
line[o++] = '\b';
|
||||
break;
|
||||
case 'f':
|
||||
line[o++] = '\f';
|
||||
break;
|
||||
case 'n':
|
||||
line[o++] = '\n';
|
||||
break;
|
||||
case 'r':
|
||||
line[o++] = '\r';
|
||||
break;
|
||||
case 't':
|
||||
line[o++] = '\t';
|
||||
break;
|
||||
case 'v':
|
||||
line[o++] = '\v';
|
||||
break;
|
||||
default:
|
||||
// '\*' '\?' '\[' '\\' will be processed during regex translation
|
||||
// '\\' is intentionally not expanded here (to avoid '\\*' and '\*'
|
||||
// ending up meaning the same thing)
|
||||
line[o++] = line[i];
|
||||
line[o++] = line[i + 1];
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
} else {
|
||||
line[o++] = line[i];
|
||||
}
|
||||
}
|
||||
input.resize(o);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if filename is considered reserved by Windows
|
||||
* @param filename filename
|
||||
* @return true if file is reserved, false otherwise
|
||||
*/
|
||||
QSFERA_SYNC_EXPORT bool csync_is_windows_reserved_word(QStringView filename)
|
||||
{
|
||||
size_t len_filename = filename.size();
|
||||
|
||||
// Drive letters
|
||||
if (len_filename == 2 && filename.at(1) == QLatin1Char(':')) {
|
||||
if (filename.at(0) >= QLatin1Char('a') && filename.at(0) <= QLatin1Char('z')) {
|
||||
return true;
|
||||
}
|
||||
if (filename.at(0) >= QLatin1Char('A') && filename.at(0) <= QLatin1Char('Z')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// win_reserved_words is ordered so we can stop once we encounter a longer word
|
||||
Q_ASSERT(std::is_sorted(
|
||||
std::begin(win_device_names), std::end(win_device_names), [](const QLatin1String &a, const QLatin1String &b) { return a.size() < b.size(); }));
|
||||
for (const auto &word : win_device_names) {
|
||||
if (static_cast<size_t>(word.size()) > len_filename) {
|
||||
break;
|
||||
}
|
||||
// until windows 11, not only the device names where illegal file names
|
||||
// also COM9.png was illegal
|
||||
if ((static_cast<size_t>(word.size()) == len_filename || filename.at(word.size()) == QLatin1Char('.'))
|
||||
&& filename.startsWith(word, Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// win_reserved_words is ordered so we can stop once we encounter a longer word
|
||||
Q_ASSERT(std::is_sorted(
|
||||
std::begin(win_system_files), std::end(win_system_files), [](const QLatin1String &a, const QLatin1String &b) { return a.size() < b.size(); }));
|
||||
for (const auto &word : win_system_files) {
|
||||
if (static_cast<size_t>(word.size()) > len_filename) {
|
||||
break;
|
||||
}
|
||||
if (static_cast<size_t>(word.size()) == len_filename && filename.compare(word, Qt::CaseInsensitive) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static CSYNC_EXCLUDE_TYPE _csync_excluded_common(QStringView path, bool excludeConflictFiles)
|
||||
{
|
||||
/* split up the path */
|
||||
QStringView bname(path);
|
||||
int lastSlash = path.lastIndexOf(QLatin1Char('/'));
|
||||
if (lastSlash >= 0) {
|
||||
bname = path.mid(lastSlash + 1);
|
||||
}
|
||||
|
||||
qsizetype blen = bname.size();
|
||||
// 9 = strlen(".sync_.db")
|
||||
if (blen >= 9 && bname.at(0) == QLatin1Char('.')) {
|
||||
if (bname.contains(QLatin1String(".db"))) {
|
||||
if (bname.startsWith(QLatin1String("._sync_"), Qt::CaseInsensitive) // "._sync_*.db*"
|
||||
|| bname.startsWith(QLatin1String(".sync_"), Qt::CaseInsensitive) // ".sync_*.db*"
|
||||
|| bname.startsWith(QLatin1String(".csync_journal.db"), Qt::CaseInsensitive)) { // ".csync_journal.db*"
|
||||
return CSYNC_FILE_SILENTLY_EXCLUDED;
|
||||
}
|
||||
}
|
||||
if (bname.startsWith(QLatin1String(".QSferaSync.log"), Qt::CaseInsensitive)) { // ".QSferaSync.log*"
|
||||
return CSYNC_FILE_SILENTLY_EXCLUDED;
|
||||
}
|
||||
}
|
||||
|
||||
// check the strlen and ignore the file if its name is longer than 254 chars.
|
||||
// whenever changing this also check createDownloadTmpFileName
|
||||
if (blen > 254) {
|
||||
return CSYNC_FILE_EXCLUDE_LONG_FILENAME;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Windows cannot sync files ending in spaces (#2176). It also cannot
|
||||
// distinguish files ending in '.' from files without an ending,
|
||||
// as '.' is a separator that is not stored internally, so let's
|
||||
// not allow to sync those to avoid file loss/ambiguities (#416)
|
||||
if (blen > 1) {
|
||||
if (bname.at(blen - 1) == QLatin1Char(' ')) {
|
||||
return CSYNC_FILE_EXCLUDE_TRAILING_SPACE;
|
||||
} else if (bname.at(blen - 1) == QLatin1Char('.')) {
|
||||
return CSYNC_FILE_EXCLUDE_INVALID_CHAR;
|
||||
}
|
||||
}
|
||||
|
||||
if (csync_is_windows_reserved_word(bname)) {
|
||||
return CSYNC_FILE_EXCLUDE_INVALID_CHAR;
|
||||
}
|
||||
|
||||
// Filter out characters not allowed in a filename on windows
|
||||
// see https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
|
||||
for (auto c : path) {
|
||||
if (c.unicode() < 32) {
|
||||
return CSYNC_FILE_EXCLUDE_INVALID_CHAR;
|
||||
}
|
||||
if (std::find_if(OCC::FileSystem::IllegalFilenameCharsWindows.begin(), OCC::FileSystem::IllegalFilenameCharsWindows.end(),
|
||||
[c](const auto illegal) { return c == illegal; })
|
||||
!= OCC::FileSystem::IllegalFilenameCharsWindows.end()) {
|
||||
return CSYNC_FILE_EXCLUDE_INVALID_CHAR;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Do not sync desktop.ini files anywhere in the tree. */
|
||||
if (bname.compare("desktop.ini"_L1, Qt::CaseInsensitive) == 0) {
|
||||
return CSYNC_FILE_SILENTLY_EXCLUDED;
|
||||
}
|
||||
|
||||
|
||||
if (excludeConflictFiles && OCC::Utility::isConflictFile(path)) {
|
||||
return CSYNC_FILE_EXCLUDE_CONFLICT;
|
||||
}
|
||||
return CSYNC_NOT_EXCLUDED;
|
||||
}
|
||||
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
ExcludedFiles::ExcludedFiles()
|
||||
: _clientVersion(OCC::Version::version())
|
||||
{
|
||||
// Windows used to use PathMatchSpec which allows *foo to match abc/deffoo.
|
||||
_wildcardsMatchSlash = Utility::isWindows();
|
||||
}
|
||||
|
||||
ExcludedFiles::~ExcludedFiles() { }
|
||||
|
||||
void ExcludedFiles::addExcludeFilePath(const QString &path)
|
||||
{
|
||||
_excludeFiles.insert(path);
|
||||
}
|
||||
|
||||
void ExcludedFiles::setExcludeConflictFiles(bool onoff)
|
||||
{
|
||||
_excludeConflictFiles = onoff;
|
||||
}
|
||||
|
||||
void ExcludedFiles::addManualExclude(const QString &expr)
|
||||
{
|
||||
_manualExcludes.append(expr);
|
||||
_allExcludes.append(expr);
|
||||
prepare();
|
||||
}
|
||||
|
||||
void ExcludedFiles::clearManualExcludes()
|
||||
{
|
||||
_manualExcludes.clear();
|
||||
reloadExcludeFiles();
|
||||
}
|
||||
|
||||
void ExcludedFiles::setWildcardsMatchSlash(bool onoff)
|
||||
{
|
||||
_wildcardsMatchSlash = onoff;
|
||||
prepare();
|
||||
}
|
||||
|
||||
void ExcludedFiles::setClientVersion(const QVersionNumber &version)
|
||||
{
|
||||
_clientVersion = version;
|
||||
}
|
||||
|
||||
bool ExcludedFiles::reloadExcludeFiles()
|
||||
{
|
||||
_allExcludes.clear();
|
||||
bool success = true;
|
||||
for (const auto &file : std::as_const(_excludeFiles)) {
|
||||
QFile f(file);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
while (!f.atEnd()) {
|
||||
QByteArray line = f.readLine().trimmed();
|
||||
if (line.startsWith("#!version")) {
|
||||
if (!versionDirectiveKeepNextLine(line))
|
||||
f.readLine();
|
||||
}
|
||||
if (line.isEmpty() || line.startsWith('#'))
|
||||
continue;
|
||||
csync_exclude_expand_escapes(line);
|
||||
_allExcludes.append(QString::fromUtf8(line));
|
||||
}
|
||||
}
|
||||
_allExcludes.append(_manualExcludes);
|
||||
prepare();
|
||||
return success;
|
||||
}
|
||||
|
||||
bool ExcludedFiles::versionDirectiveKeepNextLine(const QByteArray &directive) const
|
||||
{
|
||||
if (!directive.startsWith("#!version"))
|
||||
return true;
|
||||
QByteArrayList args = directive.split(' ');
|
||||
if (args.size() != 3)
|
||||
return true;
|
||||
QByteArray op = args[1];
|
||||
QByteArrayList argVersions = args[2].split('.');
|
||||
if (argVersions.size() != 3)
|
||||
return true;
|
||||
|
||||
const auto argVersion = QVersionNumber(argVersions[0].toInt(), argVersions[1].toInt(), argVersions[2].toInt());
|
||||
if (op == "<=")
|
||||
return _clientVersion <= argVersion;
|
||||
if (op == "<")
|
||||
return _clientVersion < argVersion;
|
||||
if (op == ">")
|
||||
return _clientVersion > argVersion;
|
||||
if (op == ">=")
|
||||
return _clientVersion >= argVersion;
|
||||
if (op == "==")
|
||||
return _clientVersion == argVersion;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ExcludedFiles::isExcluded(QStringView filePath, QStringView basePath, bool excludeHidden) const
|
||||
{
|
||||
Q_ASSERT(!filePath.isEmpty());
|
||||
Q_ASSERT(!basePath.isEmpty());
|
||||
#ifdef Q_OS_WIN
|
||||
Q_ASSERT_X(!filePath.contains('\\'_L1), Q_FUNC_INFO, "ExcludedFiles does not support Windows like paths");
|
||||
Q_ASSERT_X(!basePath.contains('\\'_L1), Q_FUNC_INFO, "ExcludedFiles does not support Windows like paths");
|
||||
#endif
|
||||
const auto isChild = FileSystem::isChildPathOf2(filePath, basePath);
|
||||
if (isChild == FileSystem::ChildResult::IsNoChild) {
|
||||
// Mark paths we're not responsible for as excluded...
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isChild & FileSystem::ChildResult::IsEqual) {
|
||||
// both paths are equal
|
||||
return false;
|
||||
}
|
||||
|
||||
const QFileInfo fileInfo(filePath.toString());
|
||||
if (!fileInfo.exists()) {
|
||||
if (excludeHidden && fileInfo.fileName().startsWith(QLatin1Char('.'))) {
|
||||
return true;
|
||||
}
|
||||
// recursively call isExcluded with the path of non-existing file's parent directory
|
||||
return isExcluded(fileInfo.path(), basePath, excludeHidden);
|
||||
}
|
||||
|
||||
if (excludeHidden) {
|
||||
QFileInfo fi = fileInfo;
|
||||
// Check all path subcomponents, but to *not* check the base path:
|
||||
// We do want to be able to sync with a hidden folder as the target.
|
||||
while (fi.filePath().size() > basePath.size()) {
|
||||
if (fi.isHidden()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the parent path
|
||||
fi = QFileInfo{fi.path()};
|
||||
}
|
||||
}
|
||||
ItemType type = ItemTypeFile;
|
||||
if (fileInfo.isDir()) {
|
||||
type = ItemTypeDirectory;
|
||||
}
|
||||
return isExcludedRemote(filePath, basePath, excludeHidden, type);
|
||||
}
|
||||
|
||||
bool ExcludedFiles::isExcludedRemote(QStringView filePath, QStringView basePath, bool excludeHidden, ItemType type) const
|
||||
{
|
||||
Q_ASSERT(!filePath.isEmpty());
|
||||
Q_ASSERT(!basePath.isEmpty());
|
||||
#ifdef Q_OS_WIN
|
||||
Q_ASSERT_X(!filePath.contains('\\'_L1), Q_FUNC_INFO, "ExcludedFiles does not support Windows like paths");
|
||||
Q_ASSERT_X(!basePath.contains('\\'_L1), Q_FUNC_INFO, "ExcludedFiles does not support Windows like paths");
|
||||
#endif
|
||||
|
||||
const auto isChild = FileSystem::isChildPathOf2(filePath, basePath);
|
||||
if (isChild == FileSystem::ChildResult::IsNoChild) {
|
||||
// Mark paths we're not responsible for as excluded...
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isChild & FileSystem::ChildResult::IsEqual) {
|
||||
// both paths are equal
|
||||
return false;
|
||||
}
|
||||
|
||||
auto relativePath = filePath.mid(basePath.size());
|
||||
Q_ASSERT(!relativePath.isEmpty());
|
||||
if (relativePath.endsWith(QLatin1Char('/'))) {
|
||||
relativePath.chop(1);
|
||||
}
|
||||
|
||||
if (excludeHidden) {
|
||||
// Check their parent directories for . hidden files
|
||||
if (relativePath.startsWith(QLatin1Char('.')) || relativePath.contains(QLatin1String("/."))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return fullPatternMatch(relativePath, type) != CSYNC_NOT_EXCLUDED;
|
||||
}
|
||||
|
||||
CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(QStringView path, ItemType filetype) const
|
||||
{
|
||||
auto match = _csync_excluded_common(path, _excludeConflictFiles);
|
||||
if (match != CSYNC_NOT_EXCLUDED)
|
||||
return match;
|
||||
if (_allExcludes.isEmpty())
|
||||
return CSYNC_NOT_EXCLUDED;
|
||||
|
||||
// Check the bname part of the path to see whether the full
|
||||
// regex should be run.
|
||||
QStringView bnameStr(path);
|
||||
int lastSlash = path.lastIndexOf(QLatin1Char('/'));
|
||||
if (lastSlash >= 0) {
|
||||
bnameStr = path.mid(lastSlash + 1);
|
||||
}
|
||||
|
||||
QRegularExpressionMatch m;
|
||||
if (filetype == ItemTypeDirectory) {
|
||||
m = _bnameTraversalRegexDir.matchView(bnameStr);
|
||||
} else {
|
||||
m = _bnameTraversalRegexFile.matchView(bnameStr);
|
||||
}
|
||||
if (!m.hasMatch())
|
||||
return CSYNC_NOT_EXCLUDED;
|
||||
if (m.capturedStart(QStringLiteral("exclude")) != -1) {
|
||||
return CSYNC_FILE_EXCLUDE_LIST;
|
||||
} else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) {
|
||||
return CSYNC_FILE_EXCLUDE_AND_REMOVE;
|
||||
}
|
||||
|
||||
// third capture: full path matching is triggered
|
||||
QStringView pathStr = path;
|
||||
|
||||
if (filetype == ItemTypeDirectory) {
|
||||
m = _fullTraversalRegexDir.matchView(pathStr);
|
||||
} else {
|
||||
m = _fullTraversalRegexFile.matchView(pathStr);
|
||||
}
|
||||
if (m.hasMatch()) {
|
||||
if (m.capturedStart(QStringLiteral("exclude")) != -1) {
|
||||
return CSYNC_FILE_EXCLUDE_LIST;
|
||||
} else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) {
|
||||
return CSYNC_FILE_EXCLUDE_AND_REMOVE;
|
||||
}
|
||||
}
|
||||
return CSYNC_NOT_EXCLUDED;
|
||||
}
|
||||
|
||||
CSYNC_EXCLUDE_TYPE ExcludedFiles::fullPatternMatch(QStringView p, ItemType filetype) const
|
||||
{
|
||||
auto match = _csync_excluded_common(p, _excludeConflictFiles);
|
||||
if (match != CSYNC_NOT_EXCLUDED)
|
||||
return match;
|
||||
if (_allExcludes.isEmpty())
|
||||
return CSYNC_NOT_EXCLUDED;
|
||||
|
||||
QRegularExpressionMatch m;
|
||||
if (filetype == ItemTypeDirectory) {
|
||||
m = _fullRegexDir.matchView(p);
|
||||
} else {
|
||||
m = _fullRegexFile.matchView(p);
|
||||
}
|
||||
if (m.hasMatch()) {
|
||||
if (m.capturedStart(QStringLiteral("exclude")) != -1) {
|
||||
return CSYNC_FILE_EXCLUDE_LIST;
|
||||
} else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) {
|
||||
return CSYNC_FILE_EXCLUDE_AND_REMOVE;
|
||||
}
|
||||
}
|
||||
return CSYNC_NOT_EXCLUDED;
|
||||
}
|
||||
|
||||
/**
|
||||
* On linux we used to use fnmatch with FNM_PATHNAME, but the windows function we used
|
||||
* didn't have that behavior. wildcardsMatchSlash can be used to control which behavior
|
||||
* the resulting regex shall use.
|
||||
*/
|
||||
QString ExcludedFiles::convertToRegexpSyntax(QString exclude, bool wildcardsMatchSlash)
|
||||
{
|
||||
// Translate *, ?, [...] to their regex variants.
|
||||
// The escape sequences \*, \?, \[. \\ have a special meaning,
|
||||
// the other ones have already been expanded before
|
||||
// (like "\\n" being replaced by "\n").
|
||||
//
|
||||
// QString being UTF-16 makes unicode-correct escaping tricky.
|
||||
// If we escaped each UTF-16 code unit we'd end up splitting 4-byte
|
||||
// code points. To avoid problems we delegate as much work as possible to
|
||||
// QRegularExpression::escape(): It always receives as long a sequence
|
||||
// as code units as possible.
|
||||
QString regex;
|
||||
int i = 0;
|
||||
int charsToEscape = 0;
|
||||
auto flush = [&]() {
|
||||
regex.append(QRegularExpression::escape(exclude.mid(i - charsToEscape, charsToEscape)));
|
||||
charsToEscape = 0;
|
||||
};
|
||||
auto len = exclude.size();
|
||||
for (; i < len; ++i) {
|
||||
switch (exclude[i].unicode()) {
|
||||
case '*':
|
||||
flush();
|
||||
if (wildcardsMatchSlash) {
|
||||
regex.append(QLatin1String(".*"));
|
||||
} else {
|
||||
regex.append(QLatin1String("[^/]*"));
|
||||
}
|
||||
break;
|
||||
case '?':
|
||||
flush();
|
||||
if (wildcardsMatchSlash) {
|
||||
regex.append(QLatin1Char('.'));
|
||||
} else {
|
||||
regex.append(QStringLiteral("[^/]"));
|
||||
}
|
||||
break;
|
||||
case '[': {
|
||||
flush();
|
||||
// Find the end of the bracket expression
|
||||
auto j = i + 1;
|
||||
for (; j < len; ++j) {
|
||||
if (exclude[j] == QLatin1Char(']'))
|
||||
break;
|
||||
if (j != len - 1 && exclude[j] == QLatin1Char('\\') && exclude[j + 1] == QLatin1Char(']'))
|
||||
++j;
|
||||
}
|
||||
if (j == len) {
|
||||
// no matching ], just insert the escaped [
|
||||
regex.append(QStringLiteral("\\["));
|
||||
break;
|
||||
}
|
||||
// Translate [! to [^
|
||||
QString bracketExpr = exclude.mid(i, j - i + 1);
|
||||
if (bracketExpr.startsWith(QLatin1String("[!")))
|
||||
bracketExpr[1] = QLatin1Char('^');
|
||||
regex.append(bracketExpr);
|
||||
i = j;
|
||||
break;
|
||||
}
|
||||
case '\\':
|
||||
flush();
|
||||
if (i == len - 1) {
|
||||
regex.append(QStringLiteral("\\\\"));
|
||||
break;
|
||||
}
|
||||
// '\*' -> '\*', but '\z' -> '\\z'
|
||||
switch (exclude[i + 1].unicode()) {
|
||||
case '*':
|
||||
case '?':
|
||||
case '[':
|
||||
case '\\':
|
||||
regex.append(QRegularExpression::escape(exclude.mid(i + 1, 1)));
|
||||
break;
|
||||
default:
|
||||
charsToEscape += 2;
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
break;
|
||||
default:
|
||||
++charsToEscape;
|
||||
break;
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return regex;
|
||||
}
|
||||
|
||||
QString ExcludedFiles::extractBnameTrigger(const QString &exclude, bool wildcardsMatchSlash)
|
||||
{
|
||||
// We can definitely drop everything to the left of a / - that will never match
|
||||
// any bname.
|
||||
QString pattern = exclude.mid(exclude.lastIndexOf(QLatin1Char('/')) + 1);
|
||||
|
||||
// Easy case, nothing else can match a slash, so that's it.
|
||||
if (!wildcardsMatchSlash)
|
||||
return pattern;
|
||||
|
||||
// Otherwise it's more complicated. Examples:
|
||||
// - "foo*bar" can match "fooX/Xbar", pattern is "*bar"
|
||||
// - "foo*bar*" can match "fooX/XbarX", pattern is "*bar*"
|
||||
// - "foo?bar" can match "foo/bar" but also "fooXbar", pattern is "*bar"
|
||||
|
||||
auto isWildcard = [](QChar c) { return c == QLatin1Char('*') || c == QLatin1Char('?'); };
|
||||
|
||||
// First, skip wildcards on the very right of the pattern
|
||||
int i = pattern.size() - 1;
|
||||
while (i >= 0 && isWildcard(pattern[i]))
|
||||
--i;
|
||||
|
||||
// Then scan further until the next wildcard that could match a /
|
||||
while (i >= 0 && !isWildcard(pattern[i]))
|
||||
--i;
|
||||
|
||||
// Everything to the right is part of the pattern
|
||||
pattern = pattern.mid(i + 1);
|
||||
|
||||
// And if there was a wildcard, it starts with a *
|
||||
if (i >= 0)
|
||||
pattern.prepend(QLatin1Char('*'));
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
void ExcludedFiles::prepare()
|
||||
{
|
||||
// Build regular expressions for the different cases.
|
||||
//
|
||||
// To compose the _bnameTraversalRegex, _fullTraversalRegex and _fullRegex
|
||||
// patterns we collect several subgroups of patterns here.
|
||||
//
|
||||
// * The "full" group will contain all patterns that contain a non-trailing
|
||||
// slash. They only make sense in the fullRegex and fullTraversalRegex.
|
||||
// * The "bname" group contains all patterns without a non-trailing slash.
|
||||
// These need separate handling in the _fullRegex (slash-containing
|
||||
// patterns must be anchored to the front, these don't need it)
|
||||
// * The "bnameTrigger" group contains the bname part of all patterns in the
|
||||
// "full" group. These and the "bname" group become _bnameTraversalRegex.
|
||||
//
|
||||
// To complicate matters, the exclude patterns have two binary attributes
|
||||
// meaning we'll end up with 4 variants:
|
||||
// * "]" patterns mean "EXCLUDE_AND_REMOVE", they get collected in the
|
||||
// pattern strings ending in "Remove". The others go to "Keep".
|
||||
// * trailing-slash patterns match directories only. They get collected
|
||||
// in the pattern strings saying "Dir", the others go into "FileDir"
|
||||
// because they match files and directories.
|
||||
|
||||
QString fullFileDirKeep;
|
||||
QString fullFileDirRemove;
|
||||
QString fullDirKeep;
|
||||
QString fullDirRemove;
|
||||
|
||||
QString bnameFileDirKeep;
|
||||
QString bnameFileDirRemove;
|
||||
QString bnameDirKeep;
|
||||
QString bnameDirRemove;
|
||||
|
||||
QString bnameTriggerFileDir;
|
||||
QString bnameTriggerDir;
|
||||
|
||||
auto regexAppend = [](QString &fileDirPattern, QString &dirPattern, const QString &appendMe, bool dirOnly) {
|
||||
QString &pattern = dirOnly ? dirPattern : fileDirPattern;
|
||||
if (!pattern.isEmpty())
|
||||
pattern.append(QLatin1Char('|'));
|
||||
pattern.append(appendMe);
|
||||
};
|
||||
|
||||
for (auto exclude : std::as_const(_allExcludes)) {
|
||||
if (exclude[0] == QLatin1Char('\n'))
|
||||
continue; // empty line
|
||||
if (exclude[0] == QLatin1Char('\r'))
|
||||
continue; // empty line
|
||||
|
||||
bool matchDirOnly = exclude.endsWith(QLatin1Char('/'));
|
||||
if (matchDirOnly)
|
||||
exclude = exclude.left(exclude.size() - 1);
|
||||
|
||||
bool removeExcluded = (exclude[0] == QLatin1Char(']'));
|
||||
if (removeExcluded)
|
||||
exclude = exclude.mid(1);
|
||||
|
||||
bool fullPath = exclude.contains(QLatin1Char('/'));
|
||||
|
||||
/* Use QRegularExpression, append to the right pattern */
|
||||
auto &bnameFileDir = removeExcluded ? bnameFileDirRemove : bnameFileDirKeep;
|
||||
auto &bnameDir = removeExcluded ? bnameDirRemove : bnameDirKeep;
|
||||
auto &fullFileDir = removeExcluded ? fullFileDirRemove : fullFileDirKeep;
|
||||
auto &fullDir = removeExcluded ? fullDirRemove : fullDirKeep;
|
||||
|
||||
auto regexExclude = convertToRegexpSyntax(exclude, _wildcardsMatchSlash);
|
||||
if (!fullPath) {
|
||||
regexAppend(bnameFileDir, bnameDir, regexExclude, matchDirOnly);
|
||||
} else {
|
||||
regexAppend(fullFileDir, fullDir, regexExclude, matchDirOnly);
|
||||
|
||||
// For activation, trigger on the 'bname' part of the full pattern.
|
||||
QString bnameExclude = extractBnameTrigger(exclude, _wildcardsMatchSlash);
|
||||
auto regexBname = convertToRegexpSyntax(bnameExclude, true);
|
||||
regexAppend(bnameTriggerFileDir, bnameTriggerDir, regexBname, matchDirOnly);
|
||||
}
|
||||
}
|
||||
|
||||
// The empty pattern would match everything - change it to match-nothing
|
||||
auto emptyMatchNothing = [](QString &pattern) {
|
||||
if (pattern.isEmpty())
|
||||
pattern = QStringLiteral("a^");
|
||||
};
|
||||
emptyMatchNothing(fullFileDirKeep);
|
||||
emptyMatchNothing(fullFileDirRemove);
|
||||
emptyMatchNothing(fullDirKeep);
|
||||
emptyMatchNothing(fullDirRemove);
|
||||
|
||||
emptyMatchNothing(bnameFileDirKeep);
|
||||
emptyMatchNothing(bnameFileDirRemove);
|
||||
emptyMatchNothing(bnameDirKeep);
|
||||
emptyMatchNothing(bnameDirRemove);
|
||||
|
||||
emptyMatchNothing(bnameTriggerFileDir);
|
||||
emptyMatchNothing(bnameTriggerDir);
|
||||
|
||||
// The bname regex is applied to the bname only, so it must be
|
||||
// anchored in the beginning and in the end. It has the structure:
|
||||
// (exclude)|(excluderemove)|(bname triggers).
|
||||
// If the third group matches, the fullActivatedRegex needs to be applied
|
||||
// to the full path.
|
||||
_bnameTraversalRegexFile.setPattern(QStringLiteral("^(?P<exclude>%1)$|"
|
||||
"^(?P<excluderemove>%2)$|"
|
||||
"^(?P<trigger>%3)$")
|
||||
.arg(bnameFileDirKeep, bnameFileDirRemove, bnameTriggerFileDir));
|
||||
_bnameTraversalRegexDir.setPattern(QStringLiteral("^(?P<exclude>%1|%2)$|"
|
||||
"^(?P<excluderemove>%3|%4)$|"
|
||||
"^(?P<trigger>%5|%6)$")
|
||||
.arg(bnameFileDirKeep, bnameDirKeep, bnameFileDirRemove, bnameDirRemove, bnameTriggerFileDir, bnameTriggerDir));
|
||||
|
||||
// The full traveral regex is applied to the full path if the trigger capture of
|
||||
// the bname regex matches. Its basic form is (exclude)|(excluderemove)".
|
||||
// This pattern can be much simpler than fullRegex since we can assume a traversal
|
||||
// situation and doesn't need to look for bname patterns in parent paths.
|
||||
_fullTraversalRegexFile.setPattern(
|
||||
// Full patterns are anchored to the beginning
|
||||
QStringLiteral("^(?P<exclude>%1)(?:$|/)"
|
||||
"|"
|
||||
"^(?P<excluderemove>%2)(?:$|/)")
|
||||
.arg(fullFileDirKeep, fullFileDirRemove));
|
||||
_fullTraversalRegexDir.setPattern(QStringLiteral("^(?P<exclude>%1|%2)(?:$|/)"
|
||||
"|"
|
||||
"^(?P<excluderemove>%3|%4)(?:$|/)")
|
||||
.arg(fullFileDirKeep, fullDirKeep, fullFileDirRemove, fullDirRemove));
|
||||
|
||||
// The full regex is applied to the full path and incorporates both bname and
|
||||
// full-path patterns. It has the form "(exclude)|(excluderemove)".
|
||||
_fullRegexFile.setPattern(QStringLiteral("(?P<exclude>"
|
||||
// Full patterns are anchored to the beginning
|
||||
"^(?:%1)(?:$|/)|"
|
||||
// Simple bname patterns can be any path component
|
||||
"(?:^|/)(?:%2)(?:$|/)|"
|
||||
// When checking a file for exclusion we must check all parent paths
|
||||
// against the dir-only patterns as well.
|
||||
"(?:^|/)(?:%3)/)"
|
||||
"|"
|
||||
"(?P<excluderemove>"
|
||||
"^(?:%4)(?:$|/)|"
|
||||
"(?:^|/)(?:%5)(?:$|/)|"
|
||||
"(?:^|/)(?:%6)/)")
|
||||
.arg(fullFileDirKeep, bnameFileDirKeep, bnameDirKeep, fullFileDirRemove, bnameFileDirRemove, bnameDirRemove));
|
||||
_fullRegexDir.setPattern(
|
||||
QStringLiteral("(?P<exclude>"
|
||||
"^(?:%1|%2)(?:$|/)|"
|
||||
"(?:^|/)(?:%3|%4)(?:$|/))"
|
||||
"|"
|
||||
"(?P<excluderemove>"
|
||||
"^(?:%5|%6)(?:$|/)|"
|
||||
"(?:^|/)(?:%7|%8)(?:$|/))")
|
||||
.arg(fullFileDirKeep, fullDirKeep, bnameFileDirKeep, bnameDirKeep, fullFileDirRemove, fullDirRemove, bnameFileDirRemove, bnameDirRemove));
|
||||
|
||||
QRegularExpression::PatternOptions patternOptions = QRegularExpression::NoPatternOption;
|
||||
if (OCC::Utility::fsCasePreserving())
|
||||
patternOptions |= QRegularExpression::CaseInsensitiveOption;
|
||||
_bnameTraversalRegexFile.setPatternOptions(patternOptions);
|
||||
_bnameTraversalRegexFile.optimize();
|
||||
_bnameTraversalRegexDir.setPatternOptions(patternOptions);
|
||||
_bnameTraversalRegexDir.optimize();
|
||||
_fullTraversalRegexFile.setPatternOptions(patternOptions);
|
||||
_fullTraversalRegexFile.optimize();
|
||||
_fullTraversalRegexDir.setPatternOptions(patternOptions);
|
||||
_fullTraversalRegexDir.optimize();
|
||||
_fullRegexFile.setPatternOptions(patternOptions);
|
||||
_fullRegexFile.optimize();
|
||||
_fullRegexDir.setPatternOptions(patternOptions);
|
||||
_fullRegexDir.optimize();
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* libcsync -- a library to sync a directory with another
|
||||
*
|
||||
* Copyright (c) 2008-2013 by Andreas Schneider <asn@cryptomilk.org>
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#ifndef _CSYNC_EXCLUDE_H
|
||||
#define _CSYNC_EXCLUDE_H
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include "csync.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QVersionNumber>
|
||||
|
||||
#include <functional>
|
||||
|
||||
enum CSYNC_EXCLUDE_TYPE : uint8_t {
|
||||
CSYNC_NOT_EXCLUDED = 0,
|
||||
CSYNC_FILE_SILENTLY_EXCLUDED,
|
||||
CSYNC_FILE_EXCLUDE_AND_REMOVE, // TODO: is there still a difference to CSYNC_FILE_SILENTLY_EXCLUDED
|
||||
CSYNC_FILE_EXCLUDE_LIST,
|
||||
CSYNC_FILE_EXCLUDE_INVALID_CHAR,
|
||||
CSYNC_FILE_EXCLUDE_TRAILING_SPACE,
|
||||
CSYNC_FILE_EXCLUDE_LONG_FILENAME,
|
||||
CSYNC_FILE_EXCLUDE_HIDDEN,
|
||||
CSYNC_FILE_EXCLUDE_CONFLICT,
|
||||
CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED,
|
||||
CSYNC_FILE_EXCLUDE_RESERVED,
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages file/directory exclusion.
|
||||
*
|
||||
* Most commonly exclude patterns are loaded from files. See
|
||||
* addExcludeFilePath() and reloadExcludeFiles().
|
||||
*
|
||||
* Excluded files are primarily relevant for sync runs, and for
|
||||
* file watcher filtering.
|
||||
*
|
||||
* Excluded files and ignored files are the same thing. But the
|
||||
* selective sync blacklist functionality is a different thing
|
||||
* entirely.
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT ExcludedFiles : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ExcludedFiles();
|
||||
~ExcludedFiles() override;
|
||||
|
||||
/**
|
||||
* Adds a new path to a file containing exclude patterns.
|
||||
*
|
||||
* Does not load the file. Use reloadExcludeFiles() afterwards.
|
||||
*/
|
||||
void addExcludeFilePath(const QString &path);
|
||||
|
||||
/**
|
||||
* Whether conflict files shall be excluded.
|
||||
*
|
||||
* Defaults to true.
|
||||
*/
|
||||
void setExcludeConflictFiles(bool onoff);
|
||||
|
||||
/**
|
||||
* Checks whether a file or directory should be excluded.
|
||||
*
|
||||
* @param filePath the absolute path to the file
|
||||
* @param basePath folder path from which to apply exclude rules, ends with a /
|
||||
*/
|
||||
bool isExcluded(QStringView filePath, QStringView basePath, bool excludeHidden) const;
|
||||
|
||||
/**
|
||||
* Checks whether a remote file or directory should be excluded.
|
||||
*
|
||||
* @param filePath the absolute path to the file
|
||||
* @param basePath folder path from which to apply exclude rules, ends with a /
|
||||
*/
|
||||
bool isExcludedRemote(QStringView filePath, QStringView basePath, bool excludeHidden, ItemType type) const;
|
||||
|
||||
/**
|
||||
* Adds an exclude pattern.
|
||||
*
|
||||
* Primarily used in tests. Patterns added this way are preserved when
|
||||
* reloadExcludeFiles() is called.
|
||||
*/
|
||||
void addManualExclude(const QString &expr);
|
||||
|
||||
/**
|
||||
* Removes all manually added exclude patterns.
|
||||
*
|
||||
* Primarily used in tests.
|
||||
*/
|
||||
void clearManualExcludes();
|
||||
|
||||
/**
|
||||
* Adjusts behavior of wildcards. Only used for testing.
|
||||
*/
|
||||
void setWildcardsMatchSlash(bool onoff);
|
||||
|
||||
/**
|
||||
* Sets the client version, only used for testing.
|
||||
*/
|
||||
void setClientVersion(const QVersionNumber &version);
|
||||
|
||||
/**
|
||||
* @brief Check if the given path should be excluded in a traversal situation.
|
||||
*
|
||||
* It does only part of the work that full() does because it's assumed
|
||||
* that all leading directories have been run through traversal()
|
||||
* before. This can be significantly faster.
|
||||
*
|
||||
* That means for 'foo/bar/file' only ('foo/bar/file', 'file') is checked
|
||||
* against the exclude patterns.
|
||||
*
|
||||
* @param Path is folder-relative, should not start with a /.
|
||||
*
|
||||
* Note that this only matches patterns. It does not check whether the file
|
||||
* or directory pointed to is hidden (or whether it even exists).
|
||||
*/
|
||||
CSYNC_EXCLUDE_TYPE traversalPatternMatch(QStringView path, ItemType filetype) const;
|
||||
|
||||
public Q_SLOTS:
|
||||
/**
|
||||
* Reloads the exclude patterns from the registered paths.
|
||||
*/
|
||||
bool reloadExcludeFiles();
|
||||
|
||||
private:
|
||||
/**
|
||||
* Returns true if the version directive indicates the next line
|
||||
* should be skipped.
|
||||
*
|
||||
* A version directive has the form "#!version <op> <version>"
|
||||
* where <op> can be <, <=, ==, >, >= and <version> can be any version
|
||||
* like 2.5.0.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* #!version < 2.5.0
|
||||
* myexclude
|
||||
*
|
||||
* Would enable the "myexclude" pattern only for versions before 2.5.0.
|
||||
*/
|
||||
bool versionDirectiveKeepNextLine(const QByteArray &directive) const;
|
||||
|
||||
/**
|
||||
* @brief Match the exclude pattern against the full path.
|
||||
*
|
||||
* @param Path is folder-relative, should not start with a /.
|
||||
*
|
||||
* Note that this only matches patterns. It does not check whether the file
|
||||
* or directory pointed to is hidden (or whether it even exists).
|
||||
*/
|
||||
CSYNC_EXCLUDE_TYPE fullPatternMatch(QStringView path, ItemType filetype) const;
|
||||
|
||||
/**
|
||||
* Generate optimized regular expressions for the exclude patterns.
|
||||
*
|
||||
* The optimization works in two steps: First, all supported patterns are put
|
||||
* into _fullRegexFile/_fullRegexDir. These regexes can be applied to the full
|
||||
* path to determine whether it is excluded or not.
|
||||
*
|
||||
* The second is a performance optimization. The particularly common use
|
||||
* case for excludes during a sync run is "traversal": Instead of checking
|
||||
* the full path every time, we check each parent path with the traversal
|
||||
* function incrementally.
|
||||
*
|
||||
* Example: When the sync run eventually arrives at "a/b/c it can assume
|
||||
* that the traversal matching has already been run on "a", "a/b"
|
||||
* and just needs to run the traversal matcher on "a/b/c".
|
||||
*
|
||||
* The full matcher is equivalent to or-combining the traversal match results
|
||||
* of all parent paths:
|
||||
* full("a/b/c/d") == traversal("a") || traversal("a/b") || traversal("a/b/c")
|
||||
*
|
||||
* The traversal matcher can be extremely fast because it has a fast early-out
|
||||
* case: It checks the bname part of the path against _bnameTraversalRegex
|
||||
* and only runs a simplified _fullTraversalRegex on the whole path if bname
|
||||
* activation for it was triggered.
|
||||
*
|
||||
* Note: The traversal matcher will return not-excluded on some paths that the
|
||||
* full matcher would exclude. Example: "b" is excluded. traversal("b/c")
|
||||
* returns not-excluded because "c" isn't a bname activation pattern.
|
||||
*/
|
||||
void prepare();
|
||||
|
||||
static QString extractBnameTrigger(const QString &exclude, bool wildcardsMatchSlash);
|
||||
static QString convertToRegexpSyntax(QString exclude, bool wildcardsMatchSlash);
|
||||
|
||||
/// Files to load excludes from
|
||||
QSet<QString> _excludeFiles;
|
||||
|
||||
/// Exclude patterns added with addManualExclude()
|
||||
QStringList _manualExcludes;
|
||||
|
||||
/// List of all active exclude patterns
|
||||
QStringList _allExcludes;
|
||||
|
||||
/// see prepare()
|
||||
QRegularExpression _bnameTraversalRegexFile;
|
||||
QRegularExpression _bnameTraversalRegexDir;
|
||||
QRegularExpression _fullTraversalRegexFile;
|
||||
QRegularExpression _fullTraversalRegexDir;
|
||||
QRegularExpression _fullRegexFile;
|
||||
QRegularExpression _fullRegexDir;
|
||||
|
||||
bool _excludeConflictFiles = true;
|
||||
|
||||
/**
|
||||
* Whether * and ? in patterns can match a /
|
||||
*
|
||||
* Unfortunately this was how matching was done on Windows so
|
||||
* it continues to be enabled there.
|
||||
*/
|
||||
bool _wildcardsMatchSlash = false;
|
||||
|
||||
/**
|
||||
* The client version. Used to evaluate version-dependent excludes,
|
||||
* see versionDirectiveKeepNextLine().
|
||||
*/
|
||||
QVersionNumber _clientVersion;
|
||||
|
||||
friend class TestExcludedFiles;
|
||||
};
|
||||
|
||||
#endif /* _CSYNC_EXCLUDE_H */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/common/syncjournaldb.h"
|
||||
#include "libsync/discoveryinfo.h"
|
||||
#include "libsync/discoveryphase.h"
|
||||
#include "libsync/syncfileitem.h"
|
||||
|
||||
#include <deque>
|
||||
|
||||
class ExcludedFiles;
|
||||
|
||||
namespace OCC {
|
||||
class SyncJournalDb;
|
||||
|
||||
/**
|
||||
* Job that handles discovery of a directory.
|
||||
*
|
||||
* This includes:
|
||||
* - Do a DiscoverySingleDirectoryJob network job which will do a PROPFIND of this directory
|
||||
* - Stat all the entries in the local file system for this directory
|
||||
* - Merge all information (and the information from the database) in order to know what needs
|
||||
* to be done for every file within this directory.
|
||||
* - For every sub-directory within this directory, "recursively" create a new ProcessDirectoryJob.
|
||||
*
|
||||
* This job is tightly coupled with the DiscoveryPhase class.
|
||||
*
|
||||
* After being start()'ed this job will perform work asynchronously and Q_EMIT finished() when done.
|
||||
*
|
||||
* Internally, this job will call DiscoveryPhase::scheduleMoreJobs when one of its sub-jobs is
|
||||
* finished. DiscoveryPhase::scheduleMoreJobs will call processSubJobs() to continue work until
|
||||
* the job is finished.
|
||||
*
|
||||
* Results are fed outwards via the DiscoveryPhase::itemDiscovered() signal.
|
||||
*/
|
||||
class ProcessDirectoryJob : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
struct PathTuple;
|
||||
public:
|
||||
enum QueryMode {
|
||||
NormalQuery,
|
||||
ParentDontExist, // Do not query this folder because it does not exist
|
||||
ParentNotChanged, // No need to query this folder because it has not changed from what is in the DB
|
||||
InBlackList // Do not query this folder because it is in the blacklist (remote entries only)
|
||||
};
|
||||
Q_ENUM(QueryMode)
|
||||
|
||||
/** For creating the root job
|
||||
*
|
||||
* The base pin state is used if the root dir's pin state can't be retrieved.
|
||||
*/
|
||||
explicit ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _discoveryData(data)
|
||||
{
|
||||
computePinState(basePinState);
|
||||
}
|
||||
|
||||
/// For creating subjobs
|
||||
explicit ProcessDirectoryJob(const PathTuple &path, const SyncFileItemPtr &dirItem,
|
||||
QueryMode queryLocal, QueryMode queryServer,
|
||||
ProcessDirectoryJob *parent)
|
||||
: QObject(parent)
|
||||
, _dirItem(dirItem)
|
||||
, _queryServer(queryServer)
|
||||
, _queryLocal(queryLocal)
|
||||
, _discoveryData(parent->_discoveryData)
|
||||
, _currentFolder(path)
|
||||
{
|
||||
computePinState(parent->_pinState);
|
||||
}
|
||||
|
||||
void start();
|
||||
/** Start up to nbJobs, return the number of job started; Q_EMIT finished() when done */
|
||||
int processSubJobs(int nbJobs);
|
||||
|
||||
SyncFileItemPtr _dirItem;
|
||||
|
||||
private:
|
||||
|
||||
/** Structure representing a path during discovery. A same path may have different value locally
|
||||
* or on the server in case of renames.
|
||||
*
|
||||
* These strings never start or ends with slashes. They are all relative to the folder's root.
|
||||
* Usually they are all the same and are even shared instance of the same QString.
|
||||
*
|
||||
* _server and _local paths will differ if there are renames, example:
|
||||
* remote renamed A/ to B/ and local renamed A/X to A/Y then
|
||||
* target: B/Y/file
|
||||
* original: A/X/file
|
||||
* local: A/Y/file
|
||||
* server: B/X/file
|
||||
*/
|
||||
struct PathTuple
|
||||
{
|
||||
QString _original; // Path as in the DB (before the sync)
|
||||
QString _target; // Path that will be the result after the sync (and will be in the DB)
|
||||
QString _server; // Path on the server (before the sync)
|
||||
QString _local; // Path locally (before the sync)
|
||||
static QString pathAppend(const QString &base, const QString &name)
|
||||
{
|
||||
return base.isEmpty() ? name : base + QLatin1Char('/') + name;
|
||||
}
|
||||
PathTuple addName(const QString &name) const
|
||||
{
|
||||
PathTuple result;
|
||||
result._original = pathAppend(_original, name);
|
||||
auto buildString = [&](const QString &other) {
|
||||
// Optimize by trying to keep all string implicitly shared if they are the same (common case)
|
||||
return other == _original ? result._original : pathAppend(other, name);
|
||||
};
|
||||
result._target = buildString(_target);
|
||||
result._server = buildString(_server);
|
||||
result._local = buildString(_local);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/** Iterate over entries inside the directory (non-recursively).
|
||||
*
|
||||
* Called once _serverEntries and _localEntries are filled
|
||||
* Calls processFile() for each non-excluded one.
|
||||
* Will start scheduling subdir jobs when done.
|
||||
*/
|
||||
void process();
|
||||
|
||||
// return true if the file is excluded.
|
||||
// path is the full relative path of the file. localName is the base name of the local entry.
|
||||
bool handleExcluded(const QString &path, const QString &localName, bool isDirectory,
|
||||
bool isHidden, bool isSymlink);
|
||||
|
||||
/** Reconcile local/remote/db information for a single item.
|
||||
*
|
||||
* Can be a file or a directory.
|
||||
* Usually ends up emitting itemDiscovered() or creating a subdirectory job.
|
||||
*
|
||||
* This main function delegates some work to the processFile* functions.
|
||||
*/
|
||||
void processFile(const PathTuple &, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &);
|
||||
|
||||
/// processFile helper for when remote information is available, typically flows into AnalyzeLocalInfo when done
|
||||
void processFileAnalyzeRemoteInfo(const SyncFileItemPtr &item, PathTuple, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &);
|
||||
|
||||
/// processFile helper for reconciling local changes
|
||||
void processFileAnalyzeLocalInfo(const SyncFileItemPtr &item, const PathTuple &, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &, QueryMode recurseQueryServer);
|
||||
|
||||
/// processFile helper for local/remote conflicts
|
||||
void processFileConflict(const SyncFileItemPtr &item, const PathTuple &, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &);
|
||||
|
||||
/// processFile helper for common final processing
|
||||
void processFileFinalize(const SyncFileItemPtr &item, PathTuple, bool recurse, QueryMode recurseQueryLocal, QueryMode recurseQueryServer);
|
||||
|
||||
|
||||
/** Checks the permission for this item, if needed, change the item to a restoration item.
|
||||
* @return false indicate that this is an error and if it is a directory, one should not recurse
|
||||
* inside it.
|
||||
*/
|
||||
bool checkPermissions(const SyncFileItemPtr &item);
|
||||
|
||||
struct MovePermissionResult
|
||||
{
|
||||
// whether moving/renaming the source is ok
|
||||
bool sourceOk;
|
||||
// whether the destination accepts (always true for renames)
|
||||
bool destinationOk;
|
||||
// whether creating a new file/dir in the destination is ok
|
||||
bool destinationNewOk;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the move is of a specified file within this directory is allowed.
|
||||
* Return true if it is allowed, false otherwise
|
||||
*/
|
||||
MovePermissionResult checkMovePermissions(RemotePermissions srcPerm, const QString &srcPath, bool isDirectory);
|
||||
|
||||
void processBlacklisted(const PathTuple &, const LocalInfo &, const SyncJournalFileRecord &dbEntry);
|
||||
void subJobFinished();
|
||||
|
||||
/** An DB operation failed */
|
||||
void dbError();
|
||||
|
||||
/** Start a remote discovery network job
|
||||
*
|
||||
* It fills _serverNormalQueryEntries and sets _serverQueryDone when done.
|
||||
*/
|
||||
DiscoverySingleDirectoryJob *startAsyncServerQuery();
|
||||
|
||||
/** Discover the local directory
|
||||
*
|
||||
* Fills _localNormalQueryEntries.
|
||||
*/
|
||||
void startAsyncLocalQuery();
|
||||
|
||||
|
||||
/** Sets _pinState, the directory's pin state
|
||||
*
|
||||
* If the folder exists locally its state is retrieved, otherwise the
|
||||
* parent's pin state is inherited.
|
||||
*/
|
||||
void computePinState(PinState parentState);
|
||||
|
||||
QueryMode _queryServer = QueryMode::NormalQuery;
|
||||
QueryMode _queryLocal = QueryMode::NormalQuery;
|
||||
|
||||
// Holds entries that resulted from a NormalQuery
|
||||
QVector<RemoteInfo> _serverNormalQueryEntries;
|
||||
QVector<LocalInfo> _localNormalQueryEntries;
|
||||
|
||||
// Whether the local/remote directory item queries are done. Will be set
|
||||
// even even for do-nothing (!= NormalQuery) queries.
|
||||
bool _serverQueryDone = false;
|
||||
bool _localQueryDone = false;
|
||||
|
||||
RemotePermissions _rootPermissions;
|
||||
QPointer<DiscoverySingleDirectoryJob> _serverJob;
|
||||
|
||||
|
||||
/** Number of currently running async jobs.
|
||||
*
|
||||
* These "async jobs" have nothing to do with the jobs for subdirectories
|
||||
* which are being tracked by _queuedJobs and _runningJobs.
|
||||
*
|
||||
* They are jobs that need to be completed to finish processing of directory
|
||||
* entries. This variable is used to ensure this job doesn't finish while
|
||||
* these jobs are still in flight.
|
||||
*/
|
||||
int _pendingAsyncJobs = 0;
|
||||
|
||||
/** The queued and running jobs for subdirectories.
|
||||
*
|
||||
* The jobs are enqueued while processind directory entries and
|
||||
* then gradually run via calls to processSubJobs().
|
||||
*/
|
||||
std::deque<ProcessDirectoryJob *> _queuedJobs;
|
||||
QVector<ProcessDirectoryJob *> _runningJobs;
|
||||
|
||||
DiscoveryPhase *_discoveryData;
|
||||
|
||||
PathTuple _currentFolder;
|
||||
bool _childModified = false; // the directory contains modified item what would prevent deletion
|
||||
bool _childIgnored = false; // The directory contains ignored item that would prevent deletion
|
||||
PinState _pinState = PinState::Unspecified; // The directory's pin-state, see computePinState()
|
||||
|
||||
Q_SIGNALS:
|
||||
void finished();
|
||||
// The root etag of this directory was fetched
|
||||
void etag(const QString &, const QDateTime &time);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#include "libsync/discoveryinfo.h"
|
||||
|
||||
#include "libsync/filesystem.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "libsync/common/utility_win.h"
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
Q_LOGGING_CATEGORY(lcLocalInfo, "sync.discovery.localinfo", QtInfoMsg)
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
class OCC::LocalInfoData : public QSharedData
|
||||
{
|
||||
public:
|
||||
LocalInfoData() = default;
|
||||
~LocalInfoData() = default;
|
||||
|
||||
LocalInfoData(const std::filesystem::directory_entry &dirent, ItemType type)
|
||||
: _name(FileSystem::fromFilesystemPath(dirent.path().filename()))
|
||||
, _type(type)
|
||||
{
|
||||
Q_ASSERT(!dirent.is_symlink() || type == ItemTypeSymLink);
|
||||
#ifdef Q_OS_WIN
|
||||
auto h = Utility::Handle::createHandle(dirent.path(), {.followSymlinks = false});
|
||||
if (!h) {
|
||||
qCWarning(lcLocalInfo) << dirent.path().native() << h.errorMessage();
|
||||
_name.clear();
|
||||
return;
|
||||
}
|
||||
BY_HANDLE_FILE_INFORMATION fileInfo = {};
|
||||
if (!GetFileInformationByHandle(h, &fileInfo)) {
|
||||
const auto error = GetLastError();
|
||||
qCCritical(lcLocalInfo) << u"GetFileInformationByHandle failed on" << dirent.path().native() << OCC::Utility::formatWinError(error);
|
||||
_name.clear();
|
||||
return;
|
||||
}
|
||||
_isHidden = fileInfo.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN;
|
||||
_inode = ULARGE_INTEGER{{ fileInfo.nFileIndexLow, fileInfo.nFileIndexHigh }}.QuadPart;
|
||||
_size = ULARGE_INTEGER{{ fileInfo.nFileSizeLow, fileInfo.nFileSizeHigh }}.QuadPart;
|
||||
_modtime = FileSystem::fileTimeToTime_t(std::filesystem::file_time_type{std::filesystem::file_time_type::duration {
|
||||
ULARGE_INTEGER{fileInfo.ftLastWriteTime.dwLowDateTime, fileInfo.ftLastWriteTime.dwHighDateTime}.QuadPart
|
||||
}});
|
||||
#else
|
||||
struct stat sb;
|
||||
if (lstat(dirent.path().native().data(), &sb) < 0) {
|
||||
qCCritical(lcLocalInfo) << u"lstat failed on" << dirent.path().native();
|
||||
_name.clear();
|
||||
return;
|
||||
}
|
||||
_inode = sb.st_ino;
|
||||
_size = sb.st_size;
|
||||
_modtime = sb.st_mtime;
|
||||
#ifdef Q_OS_MAC
|
||||
_isHidden = sb.st_flags & UF_HIDDEN;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
QString _name;
|
||||
ItemType _type = ItemTypeUnsupported;
|
||||
time_t _modtime = 0;
|
||||
int64_t _size = 0;
|
||||
uint64_t _inode = 0;
|
||||
bool _isHidden = false;
|
||||
};
|
||||
|
||||
LocalInfo::LocalInfo()
|
||||
: d([] {
|
||||
static QExplicitlySharedDataPointer<LocalInfoData> nullData{new LocalInfoData{}};
|
||||
return nullData;
|
||||
}())
|
||||
{
|
||||
}
|
||||
|
||||
LocalInfo::LocalInfo(const std::filesystem::directory_entry &dirent, ItemType type)
|
||||
: d(new LocalInfoData(dirent, type))
|
||||
{
|
||||
}
|
||||
|
||||
LocalInfo::~LocalInfo() = default;
|
||||
|
||||
LocalInfo::LocalInfo(const LocalInfo &other) = default;
|
||||
|
||||
LocalInfo &LocalInfo::operator=(const LocalInfo &other) = default;
|
||||
|
||||
LocalInfo::LocalInfo(const std::filesystem::directory_entry &dirent)
|
||||
: LocalInfo(dirent, LocalInfo::typeFromDirectoryEntry(dirent))
|
||||
{
|
||||
}
|
||||
|
||||
LocalInfo::LocalInfo(const std::filesystem::path &path)
|
||||
: LocalInfo(std::filesystem::directory_entry{path})
|
||||
{
|
||||
}
|
||||
|
||||
ItemType LocalInfo::typeFromDirectoryEntry(const std::filesystem::directory_entry &dirent)
|
||||
{
|
||||
// a file can be a symlink but point to a regular file, so we check for symlink first
|
||||
if (dirent.is_symlink()) {
|
||||
return ItemTypeSymLink;
|
||||
}
|
||||
if (dirent.is_regular_file()) {
|
||||
return ItemTypeFile;
|
||||
}
|
||||
if (dirent.is_directory()) {
|
||||
return ItemTypeDirectory;
|
||||
}
|
||||
return ItemTypeUnsupported;
|
||||
}
|
||||
|
||||
bool LocalInfo::isHidden() const
|
||||
{
|
||||
return d->_isHidden;
|
||||
}
|
||||
|
||||
QString LocalInfo::name() const
|
||||
{
|
||||
return d->_name;
|
||||
}
|
||||
|
||||
time_t LocalInfo::modtime() const
|
||||
{
|
||||
return d->_modtime;
|
||||
}
|
||||
|
||||
int64_t LocalInfo::size() const
|
||||
{
|
||||
return d->_size;
|
||||
}
|
||||
|
||||
uint64_t LocalInfo::inode() const
|
||||
{
|
||||
return d->_inode;
|
||||
}
|
||||
|
||||
ItemType LocalInfo::type() const
|
||||
{
|
||||
return d->_type;
|
||||
}
|
||||
|
||||
bool LocalInfo::isDirectory() const
|
||||
{
|
||||
return d->_type == ItemTypeDirectory;
|
||||
}
|
||||
|
||||
bool LocalInfo::isVirtualFile() const
|
||||
{
|
||||
return d->_type == ItemTypeVirtualFile || d->_type == ItemTypeVirtualFileDownload;
|
||||
}
|
||||
|
||||
bool LocalInfo::isSymLink() const
|
||||
{
|
||||
return d->_type == ItemTypeSymLink;
|
||||
}
|
||||
|
||||
bool LocalInfo::isValid() const
|
||||
{
|
||||
return !d->_name.isNull();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#pragma once
|
||||
#include "libsync/csync.h"
|
||||
|
||||
#include <QSharedData>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class LocalInfoData;
|
||||
class QSFERA_SYNC_EXPORT LocalInfo
|
||||
{
|
||||
public:
|
||||
LocalInfo();
|
||||
~LocalInfo();
|
||||
LocalInfo(const LocalInfo &other);
|
||||
LocalInfo &operator=(const LocalInfo &other);
|
||||
|
||||
void swap(LocalInfo &other) noexcept { d.swap(other.d); }
|
||||
QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP(LocalInfo);
|
||||
|
||||
LocalInfo(const std::filesystem::directory_entry &dirent, ItemType type);
|
||||
LocalInfo(const std::filesystem::directory_entry &dirent);
|
||||
|
||||
// TODO: consume path by default
|
||||
LocalInfo(const std::filesystem::path &path);
|
||||
|
||||
|
||||
static ItemType typeFromDirectoryEntry(const std::filesystem::directory_entry &dirent);
|
||||
|
||||
bool isHidden() const;
|
||||
|
||||
/** FileName of the entry (this does not contain any directory or path, just the plain name */
|
||||
QString name() const;
|
||||
time_t modtime() const;
|
||||
int64_t size() const;
|
||||
uint64_t inode() const;
|
||||
ItemType type() const;
|
||||
bool isDirectory() const;
|
||||
bool isVirtualFile() const;
|
||||
bool isSymLink() const;
|
||||
bool isValid() const;
|
||||
|
||||
private:
|
||||
QExplicitlySharedDataPointer<LocalInfoData> d;
|
||||
};
|
||||
}
|
||||
Q_DECLARE_SHARED(OCC::LocalInfo);
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "discoveryphase.h"
|
||||
#include "discovery.h"
|
||||
|
||||
#include "account.h"
|
||||
#include "common/asserts.h"
|
||||
#include "common/checksums.h"
|
||||
#include "filesystem.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QLoggingCategory>
|
||||
#include <QUrl>
|
||||
#include <cstring>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcDiscovery, "sync.discovery", QtInfoMsg)
|
||||
|
||||
/* Given a sorted list of paths ending with '/', return whether or not the given path is within one of the paths of the list*/
|
||||
static bool findPathInList(const std::set<QString> &list, const QString &path)
|
||||
{
|
||||
if (list.size() == 1 && *list.cbegin() == QLatin1String("/")) {
|
||||
// Special case for the case "/" is there, it matches everything
|
||||
return true;
|
||||
}
|
||||
|
||||
QString pathSlash = path + QLatin1Char('/');
|
||||
|
||||
// Since the list is sorted, we can do a binary search.
|
||||
// If the path is a prefix of another item or right after in the lexical order.
|
||||
auto it = std::lower_bound(list.begin(), list.end(), pathSlash);
|
||||
|
||||
if (it != list.end() && *it == pathSlash) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (it == list.begin()) {
|
||||
return false;
|
||||
}
|
||||
--it;
|
||||
Q_ASSERT(it->endsWith(QLatin1Char('/'))); // Folder::setSelectiveSyncBlackList makes sure of that
|
||||
return pathSlash.startsWith(*it);
|
||||
}
|
||||
|
||||
bool DiscoveryPhase::isInSelectiveSyncBlackList(const QString &path) const
|
||||
{
|
||||
if (_selectiveSyncBlackList.empty()) {
|
||||
// If there is no black list, everything is allowed
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block if it is in the black list
|
||||
if (findPathInList(_selectiveSyncBlackList, path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Given a path on the remote, give the path as it is when the rename is done */
|
||||
QString DiscoveryPhase::adjustRenamedPath(const QString &original, SyncFileItem::Direction d) const
|
||||
{
|
||||
return OCC::adjustRenamedPath(d == SyncFileItem::Down ? _renamedItemsRemote : _renamedItemsLocal, original);
|
||||
}
|
||||
|
||||
QString adjustRenamedPath(const QHash<QString, QString> &renamedItems, const QString &original)
|
||||
{
|
||||
int slashPos = original.size();
|
||||
while ((slashPos = original.lastIndexOf(QLatin1Char('/'), slashPos - 1)) > 0) {
|
||||
auto it = renamedItems.constFind(original.left(slashPos));
|
||||
if (it != renamedItems.constEnd()) {
|
||||
return *it + original.mid(slashPos);
|
||||
}
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
QPair<bool, QString> DiscoveryPhase::findAndCancelDeletedJob(const QString &originalPath)
|
||||
{
|
||||
bool result = false;
|
||||
QString oldEtag;
|
||||
auto it = _deletedItem.constFind(originalPath);
|
||||
if (it != _deletedItem.cend()) {
|
||||
const auto &item = *it;
|
||||
const SyncInstructions instruction = item->instruction();
|
||||
if (instruction == CSYNC_INSTRUCTION_IGNORE && item->_type == ItemTypeVirtualFile) {
|
||||
// re-creation of virtual files count as a delete
|
||||
// restoration after a prohibited move
|
||||
// a file might be in an error state and thus gets marked as CSYNC_INSTRUCTION_IGNORE
|
||||
// after it was initially marked as CSYNC_INSTRUCTION_REMOVE
|
||||
// return true, to not trigger any additional actions on that file that could elad to dataloss
|
||||
result = true;
|
||||
oldEtag = item->_etag;
|
||||
} else {
|
||||
if (!(instruction == CSYNC_INSTRUCTION_REMOVE
|
||||
// re-creation of virtual files count as a delete
|
||||
|| (item->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW)
|
||||
|| (item->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW)
|
||||
// we encountered an ignored error
|
||||
|| (item->_hasBlacklistEntry && instruction == CSYNC_INSTRUCTION_IGNORE))) {
|
||||
qCWarning(lcDiscovery) << u"OC_ENFORCE(FAILING)" << originalPath;
|
||||
qCWarning(lcDiscovery) << u"instruction == CSYNC_INSTRUCTION_REMOVE" << (instruction == CSYNC_INSTRUCTION_REMOVE);
|
||||
qCWarning(lcDiscovery) << u"(item->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW)"
|
||||
<< (item->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW);
|
||||
qCWarning(lcDiscovery) << u"(item->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW)"
|
||||
<< (item->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW);
|
||||
qCWarning(lcDiscovery) << u"(item->_hasBlacklistEntry && instruction == CSYNC_INSTRUCTION_IGNORE)"
|
||||
<< (item->_hasBlacklistEntry && instruction == CSYNC_INSTRUCTION_IGNORE);
|
||||
qCWarning(lcDiscovery) << u"instruction" << instruction;
|
||||
qCWarning(lcDiscovery) << u"item->_type" << item->_type;
|
||||
qCWarning(lcDiscovery) << u"item->_isRestoration " << item->_isRestoration;
|
||||
qCWarning(lcDiscovery) << u"item->_remotePerm" << item->_remotePerm;
|
||||
OC_ENFORCE(false);
|
||||
}
|
||||
item->setInstruction(CSYNC_INSTRUCTION_NONE);
|
||||
result = true;
|
||||
oldEtag = item->_etag;
|
||||
}
|
||||
_deletedItem.erase(it);
|
||||
}
|
||||
if (auto *otherJob = _queuedDeletedDirectories.take(originalPath)) {
|
||||
oldEtag = otherJob->_dirItem->_etag;
|
||||
delete otherJob;
|
||||
result = true;
|
||||
}
|
||||
return { result, oldEtag };
|
||||
}
|
||||
|
||||
void DiscoveryPhase::startJob(ProcessDirectoryJob *job)
|
||||
{
|
||||
OC_ENFORCE(!_currentRootJob);
|
||||
connect(job, &ProcessDirectoryJob::finished, this, [this, job] {
|
||||
OC_ENFORCE(_currentRootJob == sender());
|
||||
_currentRootJob = nullptr;
|
||||
if (job->_dirItem)
|
||||
Q_EMIT itemDiscovered(job->_dirItem);
|
||||
job->deleteLater();
|
||||
|
||||
// Once the main job has finished recurse here to execute the remaining
|
||||
// jobs for queued deleted directories.
|
||||
if (!_queuedDeletedDirectories.isEmpty()) {
|
||||
auto nextJob = _queuedDeletedDirectories.take(_queuedDeletedDirectories.firstKey());
|
||||
startJob(nextJob);
|
||||
} else {
|
||||
Q_EMIT finished();
|
||||
}
|
||||
});
|
||||
_currentRootJob = job;
|
||||
job->start();
|
||||
}
|
||||
|
||||
void DiscoveryPhase::setSelectiveSyncBlackList(const QSet<QString> &list)
|
||||
{
|
||||
_selectiveSyncBlackList = {list.cbegin(), list.cend()};
|
||||
}
|
||||
|
||||
void DiscoveryPhase::setSelectiveSyncWhiteList(const QSet<QString> &list)
|
||||
{
|
||||
_selectiveSyncWhiteList = {list.cbegin(), list.cend()};
|
||||
}
|
||||
|
||||
void DiscoveryPhase::scheduleMoreJobs()
|
||||
{
|
||||
auto limit = std::max(1, _syncOptions._parallelNetworkJobs());
|
||||
if (_currentRootJob && _currentlyActiveJobs < limit) {
|
||||
_currentRootJob->processSubJobs(limit - _currentlyActiveJobs);
|
||||
}
|
||||
}
|
||||
|
||||
DiscoverySingleLocalDirectoryJob::DiscoverySingleLocalDirectoryJob(const AccountPtr &account, const QString &localPath, OCC::Vfs *vfs, QObject *parent)
|
||||
: QObject(parent)
|
||||
, QRunnable()
|
||||
, _localPath(localPath)
|
||||
, _account(account)
|
||||
, _vfs(vfs)
|
||||
{
|
||||
qRegisterMetaType<QVector<LocalInfo> >("QVector<LocalInfo>");
|
||||
}
|
||||
|
||||
// Use as QRunnable
|
||||
void DiscoverySingleLocalDirectoryJob::run() {
|
||||
std::error_code ec;
|
||||
const auto localPath = FileSystem::toFilesystemPath(_localPath);
|
||||
QVector<LocalInfo> results;
|
||||
for (const auto &dirent : std::filesystem::directory_iterator{localPath, ec}) {
|
||||
ItemType type = LocalInfo::typeFromDirectoryEntry(dirent);
|
||||
if (type == ItemTypeUnsupported) {
|
||||
continue;
|
||||
}
|
||||
auto info = _vfs->statTypeVirtualFile(dirent, type);
|
||||
if (!info.isValid()) {
|
||||
continue;
|
||||
}
|
||||
results.push_back(std::move(info));
|
||||
}
|
||||
if (ec) {
|
||||
qCCritical(lcDiscovery) << u"Error while opening directory" << _localPath << ec.message();
|
||||
QString errorString = tr("Error while opening directory »%1«").arg(_localPath);
|
||||
if (ec.value() == EACCES) {
|
||||
errorString = tr("Directory not accessible on client, permission denied");
|
||||
Q_EMIT finishedNonFatalError(errorString);
|
||||
return;
|
||||
} else if (ec.value() == ENOENT) {
|
||||
errorString = tr("Directory not found: »%1«").arg(_localPath);
|
||||
} else if (ec.value() == ENOTDIR) {
|
||||
// Not a directory..
|
||||
// Just consider it is empty
|
||||
return;
|
||||
}
|
||||
Q_EMIT finishedFatalError(errorString);
|
||||
return;
|
||||
}
|
||||
|
||||
Q_EMIT finished(results);
|
||||
}
|
||||
|
||||
DiscoverySingleDirectoryJob::DiscoverySingleDirectoryJob(const AccountPtr &account, const QUrl &baseUrl, const QString &path, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _subPath(path)
|
||||
, _account(account)
|
||||
, _baseUrl(baseUrl)
|
||||
, _ignoredFirst(false)
|
||||
, _isRootPath(false)
|
||||
{
|
||||
}
|
||||
|
||||
void DiscoverySingleDirectoryJob::start()
|
||||
{
|
||||
// Start the actual HTTP job
|
||||
_proFindJob = new PropfindJob(_account, _baseUrl, _subPath, PropfindJob::Depth::One, this);
|
||||
_proFindJob->setProperties({
|
||||
"resourcetype"_ba,
|
||||
"getlastmodified"_ba,
|
||||
"getcontentlength"_ba,
|
||||
"getetag"_ba,
|
||||
"http://owncloud.org/ns:id"_ba,
|
||||
"http://owncloud.org/ns:permissions"_ba,
|
||||
"http://owncloud.org/ns:checksums"_ba,
|
||||
});
|
||||
|
||||
QObject::connect(_proFindJob, &PropfindJob::directoryListingIterated,
|
||||
this, &DiscoverySingleDirectoryJob::directoryListingIteratedSlot);
|
||||
QObject::connect(_proFindJob, &PropfindJob::finishedWithError, this, [this] {
|
||||
QString msg = _proFindJob->errorString();
|
||||
if (_proFindJob->reply()->error() == QNetworkReply::NoError
|
||||
&& !_proFindJob->reply()->header(QNetworkRequest::ContentTypeHeader).toString().contains(QLatin1String("application/xml; charset=utf-8"))) {
|
||||
msg = tr("Server error: PROPFIND reply is not XML formatted!");
|
||||
}
|
||||
Q_EMIT finished(HttpError{_proFindJob->httpStatusCode(), msg});
|
||||
deleteLater();
|
||||
});
|
||||
QObject::connect(_proFindJob, &PropfindJob::finishedWithoutError, this, &DiscoverySingleDirectoryJob::lsJobFinishedWithoutErrorSlot);
|
||||
_proFindJob->start();
|
||||
}
|
||||
|
||||
void DiscoverySingleDirectoryJob::abort()
|
||||
{
|
||||
if (_proFindJob) {
|
||||
_proFindJob->abort();
|
||||
}
|
||||
}
|
||||
|
||||
void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &file, const QMap<QString, QString> &map)
|
||||
{
|
||||
if (!_ignoredFirst) {
|
||||
// The first entry is for the folder itself, we should process it differently.
|
||||
_ignoredFirst = true;
|
||||
if (auto it = Utility::optionalFind(map, QStringLiteral("permissions"))) {
|
||||
auto perm = RemotePermissions::fromServerString(it->value());
|
||||
Q_EMIT firstDirectoryPermissions(perm);
|
||||
}
|
||||
} else {
|
||||
_results.emplace_back(file, map);
|
||||
}
|
||||
|
||||
//This works in concerto with the RequestEtagJob and the Folder object to check if the remote folder changed.
|
||||
if (_firstEtag.isEmpty()) {
|
||||
if (auto it = Utility::optionalFind(map, QStringLiteral("getetag"))) {
|
||||
_firstEtag = Utility::normalizeEtag(it->value()); // for directory itself
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DiscoverySingleDirectoryJob::lsJobFinishedWithoutErrorSlot()
|
||||
{
|
||||
if (!_ignoredFirst) {
|
||||
// This is a sanity check, if we haven't _ignoredFirst then it means we never received any directoryListingIteratedSlot
|
||||
// which means somehow the server XML was bogus
|
||||
Q_EMIT finished(HttpError{0, tr("Server error: PROPFIND reply is not XML formatted!")});
|
||||
deleteLater();
|
||||
return;
|
||||
} else if (!_error.isEmpty()) {
|
||||
Q_EMIT finished(HttpError{0, _error});
|
||||
deleteLater();
|
||||
return;
|
||||
}
|
||||
Q_EMIT etag(_firstEtag, _proFindJob->responseQTimeStamp());
|
||||
Q_EMIT finished(_results);
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/discoveryremoteinfo.h"
|
||||
#include "networkjobs.h"
|
||||
#include "syncfileitem.h"
|
||||
#include "syncoptions.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QRunnable>
|
||||
#include <QStringList>
|
||||
|
||||
class ExcludedFiles;
|
||||
|
||||
namespace OCC {
|
||||
class Vfs;
|
||||
|
||||
enum class LocalDiscoveryStyle {
|
||||
FilesystemOnly, //< read all local data from the filesystem
|
||||
DatabaseAndFilesystem, //< read from the db, except for listed paths
|
||||
};
|
||||
|
||||
|
||||
class Account;
|
||||
class SyncJournalDb;
|
||||
class ProcessDirectoryJob;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Run list on a local directory and process the results for Discovery
|
||||
*
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class DiscoverySingleLocalDirectoryJob : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DiscoverySingleLocalDirectoryJob(const AccountPtr &account, const QString &localPath, OCC::Vfs *vfs, QObject *parent = nullptr);
|
||||
|
||||
void run() override;
|
||||
Q_SIGNALS:
|
||||
void finished(QVector<LocalInfo> result);
|
||||
void finishedFatalError(QString errorString);
|
||||
void finishedNonFatalError(QString errorString);
|
||||
|
||||
void itemDiscovered(SyncFileItemPtr item);
|
||||
void childIgnored(bool b);
|
||||
|
||||
private:
|
||||
QString _localPath;
|
||||
AccountPtr _account;
|
||||
OCC::Vfs *_vfs;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @brief Run a PROPFIND on a directory and process the results for Discovery
|
||||
*
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class DiscoverySingleDirectoryJob : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DiscoverySingleDirectoryJob(const AccountPtr &account, const QUrl &baseUrl, const QString &path, QObject *parent = nullptr);
|
||||
// Specify that this is the root and we need to check the data-fingerprint
|
||||
void setIsRootPath() { _isRootPath = true; }
|
||||
bool isRootPath() const { return _isRootPath; }
|
||||
void start();
|
||||
void abort();
|
||||
|
||||
// This is not actually a network job, it is just a job
|
||||
Q_SIGNALS:
|
||||
void firstDirectoryPermissions(RemotePermissions);
|
||||
void etag(const QString &, const QDateTime &time);
|
||||
void finished(const HttpResult<QVector<RemoteInfo>> &result);
|
||||
|
||||
private Q_SLOTS:
|
||||
void directoryListingIteratedSlot(const QString &, const QMap<QString, QString> &);
|
||||
void lsJobFinishedWithoutErrorSlot();
|
||||
|
||||
private:
|
||||
QVector<RemoteInfo> _results;
|
||||
QString _subPath;
|
||||
QString _firstEtag;
|
||||
AccountPtr _account;
|
||||
const QUrl _baseUrl;
|
||||
// The first result is for the directory itself and need to be ignored.
|
||||
// This flag is true if it was already ignored.
|
||||
bool _ignoredFirst;
|
||||
// Set to true if this is the root path and we need to check the data-fingerprint
|
||||
bool _isRootPath;
|
||||
// If set, the discovery will finish with an error
|
||||
QString _error;
|
||||
QPointer<PropfindJob> _proFindJob;
|
||||
};
|
||||
|
||||
class DiscoveryPhase : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class ProcessDirectoryJob;
|
||||
|
||||
QPointer<ProcessDirectoryJob> _currentRootJob;
|
||||
|
||||
/** Maps the db-path of a deleted item to its SyncFileItem.
|
||||
*
|
||||
* If it turns out the item was renamed after all, the instruction
|
||||
* can be changed. See findAndCancelDeletedJob(). Note that
|
||||
* itemDiscovered() will already have been emitted for the item.
|
||||
*/
|
||||
QHash<QString, SyncFileItemPtr> _deletedItem;
|
||||
|
||||
/** Maps the db-path of a deleted folder to its queued job.
|
||||
*
|
||||
* If a folder is deleted and must be recursed into, its job isn't
|
||||
* executed immediately. Instead it's queued here and only run
|
||||
* once the rest of the discovery has finished and we are certain
|
||||
* that the folder wasn't just renamed. This avoids running the
|
||||
* discovery on contents in the old location of renamed folders.
|
||||
*
|
||||
* See findAndCancelDeletedJob().
|
||||
*/
|
||||
// needs to be ordered
|
||||
QMap<QString, ProcessDirectoryJob *> _queuedDeletedDirectories;
|
||||
|
||||
// map source (original path) -> destinations (current server or local path)
|
||||
QHash<QString, QString> _renamedItemsRemote;
|
||||
QHash<QString, QString> _renamedItemsLocal;
|
||||
|
||||
// set of paths that should not be removed even though they are removed locally:
|
||||
// there was a move to an invalid destination and now the source should be restored
|
||||
//
|
||||
// This applies recursively to subdirectories.
|
||||
// All entries should have a trailing slash (even files), so lookup with
|
||||
// lowerBound() is reliable.
|
||||
//
|
||||
// needs to be sorted
|
||||
std::set<QString> _forbiddenDeletes;
|
||||
|
||||
/** Returns whether the db-path has been renamed locally or on the remote.
|
||||
*
|
||||
* Useful for avoiding processing of items that have already been claimed in
|
||||
* a rename (would otherwise be discovered as deletions).
|
||||
*/
|
||||
bool isRenamed(const QString &p) const { return _renamedItemsLocal.contains(p) || _renamedItemsRemote.contains(p); }
|
||||
|
||||
int _currentlyActiveJobs = 0;
|
||||
|
||||
// both must contain a sorted list
|
||||
std::set<QString> _selectiveSyncBlackList;
|
||||
std::set<QString> _selectiveSyncWhiteList;
|
||||
|
||||
void scheduleMoreJobs();
|
||||
|
||||
bool isInSelectiveSyncBlackList(const QString &path) const;
|
||||
|
||||
/** Given an original path, return the target path obtained when renaming is done.
|
||||
*
|
||||
* Note that it only considers parent directory renames. So if A/B got renamed to C/D,
|
||||
* checking A/B/file would yield C/D/file, but checking A/B would yield A/B.
|
||||
*/
|
||||
QString adjustRenamedPath(const QString &original, SyncFileItem::Direction) const;
|
||||
|
||||
/** If the db-path is scheduled for deletion, abort it.
|
||||
*
|
||||
* Check if there is already a job to delete that item:
|
||||
* If that's not the case, return { false, QByteArray() }.
|
||||
* If there is such a job, cancel that job and return true and the old etag.
|
||||
*
|
||||
* Used when having detected a rename: The rename source may have been
|
||||
* discovered before and would have looked like a delete.
|
||||
*
|
||||
* See _deletedItem and _queuedDeletedDirectories.
|
||||
*/
|
||||
QPair<bool, QString> findAndCancelDeletedJob(const QString &originalPath);
|
||||
|
||||
public:
|
||||
// input
|
||||
DiscoveryPhase(const AccountPtr &account, const SyncOptions &options, const QUrl &baseUrl, QObject *parent = nullptr)
|
||||
: QObject(parent)
|
||||
, _account(account)
|
||||
, _syncOptions(options)
|
||||
, _baseUrl(baseUrl)
|
||||
{
|
||||
}
|
||||
AccountPtr _account;
|
||||
const SyncOptions _syncOptions;
|
||||
const QUrl _baseUrl;
|
||||
QString _localDir; // absolute path to the local directory. ends with '/'
|
||||
QString _remoteFolder; // remote folder, ends with '/'
|
||||
SyncJournalDb *_statedb;
|
||||
ExcludedFiles *_excludes;
|
||||
QRegularExpression _invalidFilenameRx; // FIXME: maybe move in ExcludedFiles
|
||||
QStringList _serverBlacklistedFiles; // The blacklist from the capabilities
|
||||
bool _ignoreHiddenFiles = false;
|
||||
std::function<bool(const QString &)> _shouldDiscoverLocaly;
|
||||
|
||||
void startJob(ProcessDirectoryJob *);
|
||||
|
||||
void setSelectiveSyncBlackList(const QSet<QString> &list);
|
||||
void setSelectiveSyncWhiteList(const QSet<QString> &list);
|
||||
|
||||
// output
|
||||
bool _anotherSyncNeeded = false;
|
||||
|
||||
|
||||
Q_SIGNALS:
|
||||
void fatalError(const QString &errorString);
|
||||
void itemDiscovered(const SyncFileItemPtr &item);
|
||||
void finished();
|
||||
|
||||
/** For excluded items that don't show up in itemDiscovered()
|
||||
*
|
||||
* The path is relative to the sync folder, similar to item->_file
|
||||
*/
|
||||
void silentlyExcluded(const QString &folderPath);
|
||||
void excluded(const QString &folderPath);
|
||||
};
|
||||
|
||||
/// Implementation of DiscoveryPhase::adjustRenamedPath
|
||||
QString adjustRenamedPath(const QHash<QString, QString> &renamedItems, const QString &original);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#include "libsync/discoveryremoteinfo.h"
|
||||
|
||||
#include "libsync/common/checksums.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
using namespace OCC;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
class OCC::RemoteInfoData : public QSharedData
|
||||
{
|
||||
public:
|
||||
RemoteInfoData() = default;
|
||||
RemoteInfoData(const QString &fileName, const QMap<QString, QString> &map)
|
||||
{
|
||||
QStringList errors;
|
||||
_name = fileName.mid(fileName.lastIndexOf('/'_L1) + 1);
|
||||
|
||||
if (auto it = Utility::optionalFind(map, "resourcetype"_L1)) {
|
||||
_isDirectory = it->value().contains(QStringLiteral("collection"));
|
||||
}
|
||||
if (auto it = Utility::optionalFind(map, "getlastmodified"_L1)) {
|
||||
const auto date = Utility::parseRFC1123Date(**it);
|
||||
Q_ASSERT(date.isValid());
|
||||
_modtime = date.toSecsSinceEpoch();
|
||||
}
|
||||
if (_isDirectory) {
|
||||
_size = 0;
|
||||
} else {
|
||||
if (auto it = Utility::optionalFind(map, "getcontentlength"_L1)) {
|
||||
// See #4573, sometimes negative size values are returned
|
||||
_size = std::max<int64_t>(0, it->value().toLongLong());
|
||||
} else {
|
||||
errors.append(u"size"_s);
|
||||
}
|
||||
}
|
||||
if (auto it = Utility::optionalFind(map, "getetag"_L1)) {
|
||||
_etag = Utility::normalizeEtag(it->value());
|
||||
}
|
||||
if (auto it = Utility::optionalFind(map, "id"_L1)) {
|
||||
_fileId = it->value().toUtf8();
|
||||
}
|
||||
if (auto it = Utility::optionalFind(map, "checksums"_L1)) {
|
||||
_checksumHeader = findBestChecksum(it->value().toUtf8());
|
||||
}
|
||||
if (auto it = Utility::optionalFind(map, "permissions"_L1)) {
|
||||
_remotePerm = RemotePermissions::fromServerString(it->value());
|
||||
}
|
||||
|
||||
|
||||
if (_etag.isEmpty()) {
|
||||
errors.append(u"etag"_s);
|
||||
}
|
||||
if (_fileId.isEmpty()) {
|
||||
errors.append(u"id"_s);
|
||||
}
|
||||
if (_size != 0 && _checksumHeader.isEmpty()) {
|
||||
errors.append(u"checksum"_s);
|
||||
}
|
||||
if (_remotePerm.isNull()) {
|
||||
errors.append(u"permissions"_s);
|
||||
}
|
||||
if (!errors.empty()) {
|
||||
_error = QApplication::translate("RemoteInfo", "server reported no %1").arg(errors.join(u", "_s));
|
||||
}
|
||||
}
|
||||
|
||||
QString _name;
|
||||
QString _etag;
|
||||
QByteArray _fileId;
|
||||
QByteArray _checksumHeader;
|
||||
RemotePermissions _remotePerm;
|
||||
time_t _modtime = 0;
|
||||
int64_t _size = 0;
|
||||
bool _isDirectory = false;
|
||||
|
||||
QString _error;
|
||||
};
|
||||
|
||||
RemoteInfo::RemoteInfo()
|
||||
: d([] {
|
||||
static QExplicitlySharedDataPointer<RemoteInfoData> nullData{new RemoteInfoData{}};
|
||||
return nullData;
|
||||
}())
|
||||
{
|
||||
}
|
||||
|
||||
RemoteInfo::RemoteInfo(const QString &fileName, const QMap<QString, QString> &map)
|
||||
: d(new RemoteInfoData(fileName, map))
|
||||
{
|
||||
}
|
||||
|
||||
RemoteInfo::~RemoteInfo() = default;
|
||||
|
||||
RemoteInfo::RemoteInfo(const RemoteInfo &other) = default;
|
||||
|
||||
RemoteInfo &RemoteInfo::operator=(const RemoteInfo &other) = default;
|
||||
|
||||
QString RemoteInfo::name() const
|
||||
{
|
||||
return d->_name;
|
||||
}
|
||||
|
||||
bool RemoteInfo::isDirectory() const
|
||||
{
|
||||
return d->_isDirectory;
|
||||
}
|
||||
|
||||
QString RemoteInfo::etag() const
|
||||
{
|
||||
return d->_etag;
|
||||
}
|
||||
|
||||
QByteArray RemoteInfo::fileId() const
|
||||
{
|
||||
return d->_fileId;
|
||||
}
|
||||
|
||||
QByteArray RemoteInfo::checksumHeader() const
|
||||
{
|
||||
return d->_checksumHeader;
|
||||
}
|
||||
|
||||
RemotePermissions RemoteInfo::remotePerm() const
|
||||
{
|
||||
return d->_remotePerm;
|
||||
}
|
||||
|
||||
time_t RemoteInfo::modtime() const
|
||||
{
|
||||
return d->_modtime;
|
||||
}
|
||||
|
||||
int64_t RemoteInfo::size() const
|
||||
{
|
||||
return d->_size;
|
||||
}
|
||||
|
||||
QString RemoteInfo::error() const
|
||||
{
|
||||
return d->_error;
|
||||
}
|
||||
|
||||
bool RemoteInfo::isValid() const
|
||||
{
|
||||
return !d->_name.isNull();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/common/remotepermissions.h"
|
||||
|
||||
#include <QSharedData>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class RemoteInfoData;
|
||||
|
||||
/**
|
||||
* Represent all the meta-data about a file in the server
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT RemoteInfo
|
||||
{
|
||||
public:
|
||||
RemoteInfo();
|
||||
RemoteInfo(const QString &fileName, const QMap<QString, QString> &map);
|
||||
~RemoteInfo();
|
||||
RemoteInfo(const RemoteInfo &other);
|
||||
RemoteInfo &operator=(const RemoteInfo &other);
|
||||
|
||||
void swap(RemoteInfo &other) noexcept { d.swap(other.d); }
|
||||
QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP(RemoteInfo);
|
||||
|
||||
/** FileName of the entry (this does not contain any directory or path, just the plain name */
|
||||
QString name() const;
|
||||
bool isDirectory() const;
|
||||
|
||||
QString etag() const;
|
||||
QByteArray fileId() const;
|
||||
QByteArray checksumHeader() const;
|
||||
|
||||
RemotePermissions remotePerm() const;
|
||||
time_t modtime() const;
|
||||
|
||||
int64_t size() const;
|
||||
|
||||
QString error() const;
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
private:
|
||||
QExplicitlySharedDataPointer<RemoteInfoData> d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_SHARED(OCC::RemoteInfo);
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "filesystem.h"
|
||||
|
||||
#include "common/asserts.h"
|
||||
#include "common/utility.h"
|
||||
#include "libsync/discoveryinfo.h"
|
||||
#include "libsync/xattr.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "csync.h"
|
||||
#include "syncfileitem.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#include "common/utility_win.h"
|
||||
#include <winsock2.h>
|
||||
#endif
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
static constexpr unsigned MaxValueSize = 1023; // This is without a terminating NUL character
|
||||
} // anonymous namespace
|
||||
|
||||
namespace OCC {
|
||||
|
||||
bool FileSystem::fileEquals(const QString &fn1, const QString &fn2)
|
||||
{
|
||||
// compare two files with given filename and return true if they have the same content
|
||||
QFile f1(fn1);
|
||||
QFile f2(fn2);
|
||||
if (!f1.open(QIODevice::ReadOnly) || !f2.open(QIODevice::ReadOnly)) {
|
||||
qCWarning(lcFileSystem) << u"fileEquals: Failed to open " << fn1 << u"or" << fn2;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getSize(QFileInfo{fn1}) != getSize(QFileInfo{fn2})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int BufferSize = 16 * 1024;
|
||||
QByteArray buffer1(BufferSize, 0);
|
||||
QByteArray buffer2(BufferSize, 0);
|
||||
// the files have the same size, compare all of it
|
||||
while (!f1.atEnd()) {
|
||||
f1.read(buffer1.data(), BufferSize);
|
||||
f2.read(buffer2.data(), BufferSize);
|
||||
if (buffer1 != buffer2) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
time_t FileSystem::fileTimeToTime_t(std::filesystem::file_time_type fileTime)
|
||||
{
|
||||
#ifdef HAS_CLOCK_CAST
|
||||
return std::chrono::system_clock::to_time_t(std::chrono::clock_cast<std::chrono::system_clock>(fileTime));
|
||||
#else
|
||||
const auto systemTime = std::chrono::time_point_cast<std::chrono::system_clock::duration>(std::chrono::file_clock::to_sys(fileTime));
|
||||
return std::chrono::system_clock::to_time_t(systemTime);
|
||||
#endif
|
||||
}
|
||||
std::filesystem::file_time_type FileSystem::time_tToFileTime(time_t fileTime)
|
||||
{
|
||||
#ifdef HAS_CLOCK_CAST
|
||||
return std::chrono::clock_cast<std::chrono::file_clock>(std::chrono::system_clock::from_time_t(fileTime));
|
||||
#else
|
||||
return std::chrono::file_clock::from_sys(std::chrono::system_clock::from_time_t(fileTime));
|
||||
#endif
|
||||
}
|
||||
|
||||
time_t FileSystem::getModTime(const std::filesystem::path &filename)
|
||||
{
|
||||
std::error_code rc;
|
||||
const auto fileTime = std::filesystem::last_write_time(filename, rc);
|
||||
if (rc) {
|
||||
Q_ASSERT(!rc);
|
||||
return std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
|
||||
} else {
|
||||
return fileTimeToTime_t(fileTime);
|
||||
}
|
||||
}
|
||||
|
||||
bool FileSystem::setModTime(const std::filesystem::path &filename, time_t modTime)
|
||||
{
|
||||
std::error_code rc;
|
||||
std::filesystem::last_write_time(filename, time_tToFileTime(modTime), rc);
|
||||
if (rc) {
|
||||
qCWarning(lcFileSystem) << u"Error setting mtime for" << filename << u"failed: rc" << rc.value() << u", error message:" << rc.message();
|
||||
Q_ASSERT(!rc);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FileSystem::FileChangedInfo FileSystem::FileChangedInfo::fromSyncFileItem(const SyncFileItem *const item)
|
||||
{
|
||||
return {.size = item->_size, .mtime = item->_modtime, .inode = (item->_inode == 0 ? std::optional<quint64>{} : item->_inode), .type = item->_type};
|
||||
}
|
||||
|
||||
FileSystem::FileChangedInfo FileSystem::FileChangedInfo::fromSyncFileItemPrevious(const SyncFileItem *const item)
|
||||
{
|
||||
return {.size = item->_previousSize, .mtime = item->_previousModtime, .type = item->_type};
|
||||
}
|
||||
|
||||
FileSystem::FileChangedInfo FileSystem::FileChangedInfo::fromSyncJournalFileRecord(const SyncJournalFileRecord &record)
|
||||
{
|
||||
if (!record.isValid()) {
|
||||
return {};
|
||||
}
|
||||
return {.size = record.size(), .mtime = record.modtime(), .inode = record.inode(), .type = record.type()};
|
||||
}
|
||||
|
||||
bool FileSystem::fileChanged(const std::filesystem::path &path, const FileChangedInfo &previousInfo)
|
||||
{
|
||||
const auto dirent = std::filesystem::directory_entry{path};
|
||||
if (!dirent.exists()) {
|
||||
if (previousInfo.mtime.has_value()) {
|
||||
qCDebug(lcFileSystem) << path.native() << u"was removed";
|
||||
return true;
|
||||
} else {
|
||||
// the file didn't exist and doesn't exist
|
||||
Q_ASSERT(false); // pointless call
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const auto type = LocalInfo::typeFromDirectoryEntry(dirent);
|
||||
if (previousInfo.type != ItemTypeUnsupported) {
|
||||
// only check for dir and file, as virtual files are irrelevant here
|
||||
if (previousInfo.type == ItemTypeDirectory && type == ItemTypeFile) {
|
||||
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: from dir to file";
|
||||
return true;
|
||||
}
|
||||
if (previousInfo.type == ItemTypeFile && type == ItemTypeDirectory) {
|
||||
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: from file to dir";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const auto info = LocalInfo(dirent, type);
|
||||
if (previousInfo.inode.has_value() && previousInfo.inode.value() != info.inode()) {
|
||||
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: inode" << previousInfo.inode.value() << u"<-->" << info.inode();
|
||||
return true;
|
||||
}
|
||||
if (info.isDirectory()) {
|
||||
if (previousInfo.size != 0) {
|
||||
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: from file to dir";
|
||||
return true;
|
||||
}
|
||||
} else if (info.size() != previousInfo.size) {
|
||||
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: size: " << previousInfo.size << u"<->" << info.size();
|
||||
return true;
|
||||
}
|
||||
if (info.modtime() != previousInfo.mtime) {
|
||||
qCDebug(lcFileSystem) << u"File" << path.native() << u"has changed: mtime: " << previousInfo.mtime << u"<->" << info.modtime();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path FileSystem::canonicalPath(const std::filesystem::path &p)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(p, ec) && !ec) {
|
||||
const auto normalized = p.lexically_normal();
|
||||
const auto parentPath = normalized.parent_path();
|
||||
// last invocation will return /
|
||||
if (parentPath == p) {
|
||||
return p;
|
||||
}
|
||||
if (normalized.filename().empty()) {
|
||||
return canonicalPath(parentPath);
|
||||
} else {
|
||||
return canonicalPath(parentPath) / normalized.filename();
|
||||
}
|
||||
}
|
||||
if (ec) {
|
||||
qCWarning(lcFileSystem) << "Failed to check existence of path:" << p << ec.message();
|
||||
}
|
||||
const auto out = std::filesystem::canonical(p, ec);
|
||||
if (ec) {
|
||||
qCWarning(lcFileSystem) << "Failed to canonicalize path:" << p << ec.message();
|
||||
return p;
|
||||
}
|
||||
#ifdef Q_OS_WIN
|
||||
// std::filesystem::canonical removes the ucn prefix
|
||||
const auto ucn = std::wstring(LR"(\\?\)");
|
||||
Q_ASSERT(!out.native().starts_with(ucn));
|
||||
return std::filesystem::path(ucn + out.native());
|
||||
#else
|
||||
return out;
|
||||
#endif
|
||||
}
|
||||
|
||||
QString FileSystem::canonicalPath(const QString &p)
|
||||
{
|
||||
// clean path to normalize path back to Qt form
|
||||
return FileSystem::fromFilesystemPath(canonicalPath(FileSystem::toFilesystemPath(p)));
|
||||
}
|
||||
|
||||
qint64 FileSystem::getSize(const std::filesystem::path &filename)
|
||||
{
|
||||
std::error_code ec;
|
||||
const quint64 size = std::filesystem::file_size(filename, ec);
|
||||
if (ec) {
|
||||
if (!std::filesystem::is_directory(filename)) {
|
||||
qCCritical(lcFileSystem) << u"Error getting size for" << filename << ec.value() << ec.message();
|
||||
} else {
|
||||
qCWarning(lcFileSystem) << u"Called getFileSize on a directory";
|
||||
Q_ASSERT(false);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
// Code inspired from Qt5's QDir::removeRecursively
|
||||
bool FileSystem::removeRecursively(const QString &path,
|
||||
RemoveEntryList *success,
|
||||
RemoveEntryList *locked,
|
||||
RemoveErrorList *errors)
|
||||
{
|
||||
bool allRemoved = true;
|
||||
QDirIterator di(path, QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot);
|
||||
|
||||
QString removeError;
|
||||
while (di.hasNext()) {
|
||||
di.next();
|
||||
const QFileInfo &fi = di.fileInfo();
|
||||
// The use of isSymLink here is okay:
|
||||
// we never want to go into this branch for .lnk files
|
||||
const bool isDir = fi.isDir() && !fi.isSymLink() && !FileSystem::isJunction(fi.absoluteFilePath());
|
||||
if (isDir) {
|
||||
allRemoved &= removeRecursively(path + QLatin1Char('/') + di.fileName(), success, locked, errors); // recursive
|
||||
} else {
|
||||
if (FileSystem::isFileLocked(di.filePath(), FileSystem::LockMode::Exclusive)) {
|
||||
locked->push_back({ di.filePath(), isDir });
|
||||
allRemoved = false;
|
||||
} else {
|
||||
if (FileSystem::remove(di.filePath(), &removeError)) {
|
||||
success->push_back({ di.filePath(), isDir });
|
||||
} else {
|
||||
errors->push_back({ { di.filePath(), isDir }, removeError });
|
||||
qCWarning(lcFileSystem) << u"Error removing " << di.filePath() << ':' << removeError;
|
||||
allRemoved = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allRemoved) {
|
||||
allRemoved = QDir().rmdir(path);
|
||||
if (allRemoved) {
|
||||
success->push_back({ path, true });
|
||||
} else {
|
||||
errors->push_back({ { path, true }, QCoreApplication::translate("FileSystem", "Could not remove folder") });
|
||||
qCWarning(lcFileSystem) << u"Error removing folder" << path;
|
||||
}
|
||||
}
|
||||
return allRemoved;
|
||||
}
|
||||
|
||||
std::optional<uint64_t> FileSystem::getInode(const std::filesystem::path &filename)
|
||||
{
|
||||
const LocalInfo info(filename);
|
||||
Q_ASSERT(info.isValid());
|
||||
if (!info.isValid()) {
|
||||
return {};
|
||||
}
|
||||
return info.inode();
|
||||
}
|
||||
|
||||
std::optional<QString> FileSystem::Tags::get(const QString &path, const QString &key)
|
||||
{
|
||||
#if defined(Q_OS_MAC) || defined(Q_OS_LINUX)
|
||||
QString platformKey = key;
|
||||
if (Utility::isLinux()) {
|
||||
platformKey = QStringLiteral("user.") + platformKey;
|
||||
}
|
||||
if (const auto d = Xattr::getxattr(toFilesystemPath(path), platformKey)) {
|
||||
return QString::fromUtf8(*d);
|
||||
}
|
||||
#elif defined(Q_OS_WIN)
|
||||
QFile file(QStringLiteral("%1:%2").arg(path, key));
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
return QString::fromUtf8(file.readAll());
|
||||
}
|
||||
#endif // Q_OS_MAC || Q_OS_LINUX
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
OCC::Result<void, QString> FileSystem::Tags::set(const QString &path, const QString &key, const QString &value)
|
||||
{
|
||||
OC_ASSERT(value.size() < MaxValueSize)
|
||||
|
||||
#if defined(Q_OS_MAC) || defined(Q_OS_LINUX)
|
||||
QString platformKey = key;
|
||||
if (Utility::isLinux()) {
|
||||
platformKey = QStringLiteral("user.") + platformKey;
|
||||
}
|
||||
return Xattr::setxattr(toFilesystemPath(path), platformKey, value.toUtf8());
|
||||
#elif defined(Q_OS_WIN)
|
||||
QFile file(QStringLiteral("%1:%2").arg(path, key));
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
return file.errorString();
|
||||
}
|
||||
const auto data = value.toUtf8();
|
||||
auto bytesWritten = file.write(data);
|
||||
if (bytesWritten != data.size()) {
|
||||
return QStringLiteral("wrote %1 out of %2 bytes").arg(QString::number(bytesWritten), QString::number(data.size()));
|
||||
}
|
||||
|
||||
return {};
|
||||
#else
|
||||
return u"Not implemented"_s;
|
||||
#endif // Q_OS_MAC || Q_OS_LINUX
|
||||
}
|
||||
|
||||
OCC::Result<void, QString> FileSystem::Tags::remove(const QString &path, const QString &key)
|
||||
{
|
||||
#if defined(Q_OS_MAC) || defined(Q_OS_LINUX)
|
||||
QString platformKey = key;
|
||||
if (Utility::isLinux()) {
|
||||
platformKey = QStringLiteral("user.%1").arg(platformKey);
|
||||
}
|
||||
|
||||
return Xattr::removexattr(toFilesystemPath(path), platformKey);
|
||||
#elif defined(Q_OS_WIN)
|
||||
const auto fsPath = toFilesystemPath(u"%1:%2"_s.arg(path, key));
|
||||
std::error_code fileError;
|
||||
std::filesystem::remove(fsPath, fileError);
|
||||
if (fileError) {
|
||||
qCWarning(lcFileSystem) << u"Failed to remove tag" << key << u"from" << path << u":" << fsPath << u"error:" << fileError.message();
|
||||
return QString::fromStdString(fileError.message());
|
||||
}
|
||||
return {};
|
||||
#else
|
||||
return u"Not implemented"_s;
|
||||
#endif // Q_OS_MAC || Q_OS_LINUX
|
||||
}
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "qsferasynclib.h"
|
||||
// Chain in the base include and extend the namespace
|
||||
#include "common/filesystembase.h"
|
||||
#include "common/result.h"
|
||||
#include "libsync/csync.h"
|
||||
|
||||
class QFile;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class SyncJournal;
|
||||
class SyncFileItem;
|
||||
|
||||
/**
|
||||
* \addtogroup libsync
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief This file contains file system helper
|
||||
*/
|
||||
namespace FileSystem {
|
||||
/**
|
||||
* @brief compare two files with given filename and return true if they have the same content
|
||||
*/
|
||||
bool fileEquals(const QString &fn1, const QString &fn2);
|
||||
|
||||
|
||||
QSFERA_SYNC_EXPORT time_t fileTimeToTime_t(std::filesystem::file_time_type fileTime);
|
||||
QSFERA_SYNC_EXPORT std::filesystem::file_time_type time_tToFileTime(time_t fileTime);
|
||||
/**
|
||||
* @brief Get the mtime for a filepath
|
||||
*
|
||||
* Use this over QFileInfo::lastModified() to avoid timezone related bugs. See
|
||||
* owncloud/core#9781 for details.
|
||||
*/
|
||||
|
||||
time_t QSFERA_SYNC_EXPORT getModTime(const std::filesystem::path &filename);
|
||||
inline time_t getModTime(const QString &filename)
|
||||
{
|
||||
return getModTime(toFilesystemPath(filename));
|
||||
}
|
||||
|
||||
bool QSFERA_SYNC_EXPORT setModTime(const std::filesystem::path &filename, time_t modTime);
|
||||
inline time_t setModTime(const QString &filename, time_t modTime)
|
||||
{
|
||||
return setModTime(toFilesystemPath(filename), modTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the size for a file
|
||||
*
|
||||
* Use this over QFileInfo::size() to avoid bugs with lnk files on Windows.
|
||||
* See https://bugreports.qt.io/browse/QTBUG-24831.
|
||||
*/
|
||||
qint64 QSFERA_SYNC_EXPORT getSize(const std::filesystem::path &filename);
|
||||
inline qint64 getSize(const QFileInfo &info)
|
||||
{
|
||||
return getSize(toFilesystemPath(info.absoluteFilePath()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retrieve a file inode with csync
|
||||
*/
|
||||
[[nodiscard]] std::optional<uint64_t> QSFERA_SYNC_EXPORT getInode(const std::filesystem::path &filename);
|
||||
[[nodiscard]] inline auto getInode(const QString &filename)
|
||||
{
|
||||
return getInode(toFilesystemPath(filename));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Check if \a fileName has changed given previous size and mtime
|
||||
*
|
||||
* Nonexisting files are covered through mtime: they have an previousMtime of -1.
|
||||
*
|
||||
* @return true if the file's mtime or size are not what is expected.
|
||||
*/
|
||||
struct FileChangedInfo
|
||||
{
|
||||
static QSFERA_SYNC_EXPORT FileChangedInfo fromSyncFileItem(const SyncFileItem *const item);
|
||||
static QSFERA_SYNC_EXPORT FileChangedInfo fromSyncFileItemPrevious(const SyncFileItem *const item);
|
||||
static QSFERA_SYNC_EXPORT FileChangedInfo fromSyncJournalFileRecord(const SyncJournalFileRecord &record);
|
||||
|
||||
qint64 size = {};
|
||||
std::optional<time_t> mtime = {};
|
||||
std::optional<quint64> inode = {};
|
||||
CSyncEnums::ItemType type = CSyncEnums::ItemTypeUnsupported;
|
||||
};
|
||||
bool QSFERA_SYNC_EXPORT fileChanged(const std::filesystem::path &path, const FileChangedInfo &previousInfo);
|
||||
|
||||
|
||||
// canonicalPath returns an empty string if the file does not exist.
|
||||
// This function also works with files that does not exist and resolve the symlinks in the
|
||||
// parent directories.
|
||||
std::filesystem::path QSFERA_SYNC_EXPORT canonicalPath(const std::filesystem::path &p);
|
||||
|
||||
QString QSFERA_SYNC_EXPORT canonicalPath(const QString &p);
|
||||
|
||||
|
||||
struct RemoveEntry
|
||||
{
|
||||
const QString path;
|
||||
const bool isDir;
|
||||
};
|
||||
struct RemoveError
|
||||
{
|
||||
const RemoveEntry entry;
|
||||
const QString error;
|
||||
};
|
||||
|
||||
using RemoveEntryList = std::vector<RemoveEntry>;
|
||||
using RemoveErrorList = std::vector<RemoveError>;
|
||||
|
||||
/**
|
||||
* Removes a directory and its contents recursively
|
||||
*
|
||||
* Returns true if all removes succeeded.
|
||||
*/
|
||||
bool QSFERA_SYNC_EXPORT removeRecursively(const QString &path, RemoveEntryList *success, RemoveEntryList *locked, RemoveErrorList *errors);
|
||||
|
||||
namespace Tags {
|
||||
std::optional<QString> QSFERA_SYNC_EXPORT get(const QString &path, const QString &key);
|
||||
OCC::Result<void, QString> QSFERA_SYNC_EXPORT set(const QString &path, const QString &key, const QString &value);
|
||||
OCC::Result<void, QString> QSFERA_SYNC_EXPORT remove(const QString &path, const QString &key);
|
||||
}
|
||||
}
|
||||
|
||||
/** @} */
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#include "libsync/globalconfig.h"
|
||||
|
||||
#include "config.h"
|
||||
#include "libsync/theme.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
QUrl GlobalConfig::serverUrl()
|
||||
{
|
||||
const auto configuredUrl = getValue("Wizard/ServerUrl"
|
||||
#ifdef APPLICATION_DEFAULT_SERVER_URL
|
||||
,
|
||||
QUrl(QStringLiteral(APPLICATION_DEFAULT_SERVER_URL))
|
||||
#endif
|
||||
);
|
||||
return configuredUrl.toUrl();
|
||||
}
|
||||
|
||||
QVariant GlobalConfig::getValue(QAnyStringView param, const QVariant &defaultValue)
|
||||
{
|
||||
static const QSettings systemSettings = {
|
||||
#ifdef Q_OS_MAC
|
||||
QStringLiteral("/Library/Preferences/%1.plist").arg(Theme::instance()->orgDomainName()), QSettings::NativeFormat
|
||||
#elif defined(Q_OS_UNIX)
|
||||
QStringLiteral("/etc/%1/%1.conf").arg(Theme::instance()->appName()), QSettings::NativeFormat
|
||||
#elif defined(Q_OS_WIN)
|
||||
QStringLiteral(R"(HKEY_LOCAL_MACHINE\Software\%1\%2)").arg(Theme::instance()->vendor(), Theme::instance()->appNameGUI()), QSettings::NativeFormat
|
||||
#else
|
||||
#error "Unsupported platform"
|
||||
#endif
|
||||
};
|
||||
return systemSettings.value(param, defaultValue);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
QVariant GlobalConfig::getPolicySetting(QAnyStringView setting, const QVariant &defaultValue)
|
||||
{
|
||||
// check for policies first and return immediately if a value is found.
|
||||
QVariant out = QSettings(QStringLiteral(R"(HKEY_CURRENT_USER\Software\Policies\%1\%2)").arg(Theme::instance()->vendor(), Theme::instance()->appNameGUI()),
|
||||
QSettings::NativeFormat)
|
||||
.value(setting);
|
||||
|
||||
if (!out.isValid()) {
|
||||
out = QSettings(QStringLiteral(R"(HKEY_LOCAL_MACHINE\Software\Policies\%1\%2)").arg(Theme::instance()->vendor(), Theme::instance()->appNameGUI()),
|
||||
QSettings::NativeFormat)
|
||||
.value(setting, defaultValue);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/qsferasynclib.h"
|
||||
|
||||
#include <QVariant>
|
||||
#include <QtQmlIntegration/QtQmlIntegration>
|
||||
|
||||
namespace OCC {
|
||||
class QSFERA_SYNC_EXPORT GlobalConfig : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QUrl serverUrl READ serverUrl CONSTANT)
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
public:
|
||||
using QObject::QObject;
|
||||
|
||||
static QVariant getValue(QAnyStringView param, const QVariant &defaultValue = {});
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
static QVariant getPolicySetting(QAnyStringView policy, const QVariant &defaultValue = {});
|
||||
#endif
|
||||
|
||||
static QUrl serverUrl();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
target_sources(libsync PRIVATE
|
||||
jobs/drives.cpp
|
||||
space.cpp
|
||||
spacesmanager.cpp)
|
||||
|
||||
target_include_directories(libsync PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>)
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "drives.h"
|
||||
|
||||
#include "account.h"
|
||||
|
||||
#include <OAICollection_of_drives.h>
|
||||
#include <OAIDrive.h>
|
||||
|
||||
|
||||
using namespace OCC;
|
||||
using namespace GraphApi;
|
||||
|
||||
namespace {
|
||||
|
||||
const auto mountpointC = QLatin1String("mountpoint");
|
||||
}
|
||||
|
||||
Drives::Drives(const AccountPtr &account, QObject *parent)
|
||||
: JsonJob(account, account->url(), QStringLiteral("/graph/v1.0/me/drives"), "GET", {}, parent)
|
||||
{
|
||||
}
|
||||
|
||||
Drives::~Drives() { }
|
||||
|
||||
const QList<OpenAPI::OAIDrive> &Drives::drives() const
|
||||
{
|
||||
if (_drives.isEmpty() && parseError().error == QJsonParseError::NoError) {
|
||||
OpenAPI::OAICollection_of_drives drives;
|
||||
drives.fromJsonObject(data());
|
||||
_drives = drives.getValue();
|
||||
// At the moment we don't support mountpoints but use the Share Jail
|
||||
_drives.erase(
|
||||
std::remove_if(_drives.begin(), _drives.end(), [](const OpenAPI::OAIDrive &it) { return it.getDriveType() == mountpointC; }), _drives.end());
|
||||
}
|
||||
return _drives;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "networkjobs/jsonjob.h"
|
||||
|
||||
#include "qsferasynclib.h"
|
||||
#include <OAIDrive.h>
|
||||
|
||||
namespace OCC {
|
||||
namespace GraphApi {
|
||||
|
||||
|
||||
class QSFERA_SYNC_EXPORT Drives : public JsonJob
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Drives(const AccountPtr &account, QObject *parent = nullptr);
|
||||
~Drives();
|
||||
const QList<OpenAPI::OAIDrive> &drives() const;
|
||||
|
||||
private:
|
||||
mutable QList<OpenAPI::OAIDrive> _drives;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "space.h"
|
||||
|
||||
#include "fonticon.h"
|
||||
#include "libsync/account.h"
|
||||
#include "libsync/graphapi/spacesmanager.h"
|
||||
#include "libsync/networkjobs.h"
|
||||
#include "libsync/networkjobs/resources.h"
|
||||
|
||||
#include "resources/resources.h"
|
||||
|
||||
using namespace OCC;
|
||||
using namespace GraphApi;
|
||||
|
||||
namespace {
|
||||
|
||||
const auto personalC = QLatin1String("personal");
|
||||
|
||||
// https://github.com/cs3org/reva/blob/0cde0a3735beaa14ebdfd8988c3eb77b3c2ab0e6/pkg/utils/utils.go#L56-L59
|
||||
const auto sharesIdC = QLatin1String("a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668");
|
||||
}
|
||||
|
||||
Space::Space(SpacesManager *spacesManager, const OpenAPI::OAIDrive &drive)
|
||||
: QObject(spacesManager)
|
||||
, _spaceManager(spacesManager)
|
||||
, _image(new SpaceImage(this))
|
||||
{
|
||||
setDrive(drive);
|
||||
connect(_image, &SpaceImage::imageChanged, this, &Space::imageChanged);
|
||||
}
|
||||
|
||||
OpenAPI::OAIDrive Space::drive() const
|
||||
{
|
||||
return _drive;
|
||||
}
|
||||
|
||||
void Space::setDrive(const OpenAPI::OAIDrive &drive)
|
||||
{
|
||||
_drive = drive;
|
||||
_image->update();
|
||||
}
|
||||
|
||||
SpaceImage::SpaceImage(Space *space)
|
||||
: QObject(space)
|
||||
, _space(space)
|
||||
{
|
||||
}
|
||||
|
||||
QIcon SpaceImage::image() const
|
||||
{
|
||||
if (_image.isNull()) {
|
||||
// remix icons is not compatible with nerdfonts so the preview will be broken
|
||||
if (_space->drive().getDriveType() == personalC) {
|
||||
return Resources::FontIcon(Resources::FontIcon::FontFamily::RemixIcon, u'', Resources::FontIcon::Size::Half);
|
||||
} else if (_space->drive().getId() == sharesIdC) {
|
||||
return Resources::FontIcon(Resources::FontIcon::FontFamily::RemixIcon, u'', Resources::FontIcon::Size::Half);
|
||||
}
|
||||
return Resources::FontIcon(Resources::FontIcon::FontFamily::RemixIcon, u'', Resources::FontIcon::Size::Half);
|
||||
}
|
||||
return _image;
|
||||
}
|
||||
|
||||
QUrl SpaceImage::qmlImageUrl() const
|
||||
{
|
||||
// while the icon is being loaded we provide a different url, qml caches the icon based on the url
|
||||
if (_fetched) {
|
||||
return QUrl(QStringLiteral("image://space/%1/%2").arg(etag(), _space->id()));
|
||||
} else {
|
||||
return QUrl(QStringLiteral("image://space/invalid/%1").arg(_space->id()));
|
||||
}
|
||||
}
|
||||
|
||||
void SpaceImage::update()
|
||||
{
|
||||
const auto &special = _space->drive().getSpecial();
|
||||
const auto img = std::find_if(special.cbegin(), special.cend(), [](const auto &it) { return it.getSpecialFolder().getName() == QLatin1String("image"); });
|
||||
if (img != special.cend()) {
|
||||
_fetched = false;
|
||||
_url = QUrl(img->getWebDavUrl());
|
||||
_etag = Utility::normalizeEtag(img->getETag());
|
||||
auto job = _space->_spaceManager->account()->resourcesCache()->makeGetJob(_url, {}, _space);
|
||||
QObject::connect(job, &SimpleNetworkJob::finishedSignal, _space, [job, this] {
|
||||
_fetched = true;
|
||||
if (job->httpStatusCode() == 200) {
|
||||
_image = job->asIcon();
|
||||
Q_EMIT imageChanged();
|
||||
}
|
||||
});
|
||||
job->start();
|
||||
} else {
|
||||
// nothing to fetch
|
||||
_fetched = true;
|
||||
}
|
||||
}
|
||||
|
||||
QString Space::displayName() const
|
||||
{
|
||||
if (_drive.getDriveType() == personalC) {
|
||||
return tr("Personal");
|
||||
} else if (_drive.getId() == sharesIdC) {
|
||||
// don't call it ShareJail
|
||||
return tr("Shares");
|
||||
}
|
||||
return _drive.getName();
|
||||
}
|
||||
|
||||
uint32_t Space::priority() const
|
||||
{
|
||||
if (_drive.getDriveType() == personalC) {
|
||||
return 100;
|
||||
} else if (_drive.getId() == sharesIdC) {
|
||||
return 50;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool Space::disabled() const
|
||||
{
|
||||
// this is how disabled spaces are represented in the graph API
|
||||
return _drive.getRoot().getDeleted().getState() == QLatin1String("trashed");
|
||||
}
|
||||
|
||||
SpaceImage *Space::image() const
|
||||
{
|
||||
return _image;
|
||||
}
|
||||
|
||||
QString Space::id() const
|
||||
{
|
||||
return _drive.getRoot().getId();
|
||||
}
|
||||
|
||||
QUrl Space::webdavUrl() const
|
||||
{
|
||||
return QUrl(_drive.getRoot().getWebDavUrl());
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "qsferasynclib.h"
|
||||
|
||||
#include "libsync/accountfwd.h"
|
||||
|
||||
#include <OAIDrive.h>
|
||||
|
||||
#include <QIcon>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
namespace OCC {
|
||||
namespace GraphApi {
|
||||
class SpacesManager;
|
||||
class Space;
|
||||
|
||||
class QSFERA_SYNC_EXPORT SpaceImage : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QUrl qmlImageUrl READ qmlImageUrl NOTIFY imageChanged)
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("C++ only")
|
||||
public:
|
||||
SpaceImage(Space *space);
|
||||
|
||||
[[nodiscard]] QUrl url() const { return _url; }
|
||||
[[nodiscard]] QString etag() const { return _etag; }
|
||||
[[nodiscard]] QIcon image() const;
|
||||
|
||||
[[nodiscard]] QUrl qmlImageUrl() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void imageChanged();
|
||||
|
||||
private:
|
||||
void update();
|
||||
|
||||
QUrl _url;
|
||||
QString _etag;
|
||||
QIcon _image;
|
||||
Space *_space = nullptr;
|
||||
bool _fetched = false;
|
||||
|
||||
friend class Space;
|
||||
};
|
||||
|
||||
class QSFERA_SYNC_EXPORT Space : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(SpaceImage *image READ image NOTIFY imageChanged)
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("Spaces can only be created by the SpacesManager")
|
||||
public:
|
||||
/***
|
||||
* Returns the display name of the drive.
|
||||
* This is identical to drive.getName() for most drives.
|
||||
* Exceptions: Personal spaces
|
||||
*/
|
||||
QString displayName() const;
|
||||
|
||||
QString id() const;
|
||||
|
||||
|
||||
OpenAPI::OAIDrive drive() const;
|
||||
|
||||
/***
|
||||
* Asign a priority to a drive, used for sorting
|
||||
*/
|
||||
uint32_t priority() const;
|
||||
|
||||
/**
|
||||
* Whether a drive object has been deleted.
|
||||
*/
|
||||
bool disabled() const;
|
||||
|
||||
SpaceImage *image() const;
|
||||
|
||||
QUrl webdavUrl() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void imageChanged();
|
||||
|
||||
private:
|
||||
Space(SpacesManager *spaceManager, const OpenAPI::OAIDrive &drive);
|
||||
void setDrive(const OpenAPI::OAIDrive &drive);
|
||||
|
||||
SpacesManager *_spaceManager;
|
||||
OpenAPI::OAIDrive _drive;
|
||||
|
||||
SpaceImage *_image;
|
||||
|
||||
friend class SpacesManager;
|
||||
friend class SpaceImage;
|
||||
};
|
||||
|
||||
} // OCC
|
||||
} // GraphApi
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "spacesmanager.h"
|
||||
|
||||
#include "libsync/account.h"
|
||||
#include "libsync/creds/abstractcredentials.h"
|
||||
#include "libsync/graphapi/jobs/drives.h"
|
||||
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using namespace OCC;
|
||||
using namespace GraphApi;
|
||||
|
||||
namespace {
|
||||
constexpr auto refreshTimeoutC = 30s;
|
||||
}
|
||||
|
||||
SpacesManager::SpacesManager(Account *parent)
|
||||
: QObject(parent)
|
||||
, _account(parent)
|
||||
, _refreshTimer(new QTimer(this))
|
||||
{
|
||||
_refreshTimer->setInterval(refreshTimeoutC);
|
||||
// the timer will be restarted once we received drives data
|
||||
_refreshTimer->setSingleShot(true);
|
||||
|
||||
connect(_refreshTimer, &QTimer::timeout, this, &SpacesManager::refresh);
|
||||
connect(_account, &Account::credentialsFetched, this, &SpacesManager::refresh);
|
||||
// legacy signal which is going to be removed in 5.0
|
||||
connect(_account, &Account::credentialsAsked, this, &SpacesManager::refresh);
|
||||
}
|
||||
|
||||
void SpacesManager::refresh()
|
||||
{
|
||||
if (!OC_ENSURE(_account->accessManager())) {
|
||||
return;
|
||||
}
|
||||
if (!_account->credentials()->ready()) {
|
||||
return;
|
||||
}
|
||||
// TODO: leak the job until we fixed the onwership https://github.com/owncloud/client/issues/11203
|
||||
auto drivesJob = new Drives(_account->sharedFromThis(), nullptr);
|
||||
drivesJob->setTimeout(refreshTimeoutC);
|
||||
connect(drivesJob, &Drives::finishedSignal, this, [drivesJob, this] {
|
||||
drivesJob->deleteLater();
|
||||
if (drivesJob->httpStatusCode() == 200) {
|
||||
auto oldKeys = _spacesMap.keys();
|
||||
for (const auto &dr : drivesJob->drives()) {
|
||||
auto *space = this->space(dr.getId());
|
||||
oldKeys.removeAll(dr.getId());
|
||||
if (!space) {
|
||||
space = new Space(this, dr);
|
||||
_spacesMap.insert(dr.getId(), space);
|
||||
} else {
|
||||
space->setDrive(dr);
|
||||
}
|
||||
Q_EMIT spaceChanged(space);
|
||||
}
|
||||
for (const QString &id : oldKeys) {
|
||||
auto *oldSpace = _spacesMap.take(id);
|
||||
oldSpace->deleteLater();
|
||||
}
|
||||
if (!_ready) {
|
||||
_ready = true;
|
||||
Q_EMIT ready();
|
||||
}
|
||||
}
|
||||
Q_EMIT updated();
|
||||
_refreshTimer->start();
|
||||
});
|
||||
_refreshTimer->stop();
|
||||
drivesJob->start();
|
||||
}
|
||||
|
||||
Space *SpacesManager::space(const QString &id) const
|
||||
{
|
||||
return _spacesMap.value(id);
|
||||
}
|
||||
|
||||
Account *SpacesManager::account() const
|
||||
{
|
||||
return _account;
|
||||
}
|
||||
|
||||
QVector<Space *> SpacesManager::spaces() const
|
||||
{
|
||||
return {_spacesMap.begin(), _spacesMap.end()};
|
||||
}
|
||||
|
||||
void SpacesManager::checkReady()
|
||||
{
|
||||
// see constructor for calls to refresh
|
||||
if (_ready) {
|
||||
Q_EMIT ready();
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "qsferasynclib.h"
|
||||
|
||||
#include "libsync/accountfwd.h"
|
||||
#include "libsync/graphapi/space.h"
|
||||
|
||||
#include <OAIDrive.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QFuture>
|
||||
|
||||
class QTimer;
|
||||
|
||||
namespace OCC {
|
||||
namespace GraphApi {
|
||||
|
||||
class QSFERA_SYNC_EXPORT SpacesManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SpacesManager(Account *parent);
|
||||
|
||||
Space *space(const QString &id) const;
|
||||
|
||||
QVector<Space *> spaces() const;
|
||||
|
||||
Account *account() const;
|
||||
|
||||
/**
|
||||
* Only relevant during bootstraping or when disconnected
|
||||
*/
|
||||
void checkReady();
|
||||
|
||||
Q_SIGNALS:
|
||||
void spaceChanged(Space *space) const;
|
||||
void updated();
|
||||
void ready() const;
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
Account *_account;
|
||||
QTimer *_refreshTimer;
|
||||
QMap<QString, Space *> _spacesMap;
|
||||
bool _ready = false;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "httplogger.h"
|
||||
|
||||
#include "common/chronoelapsedtimer.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLoggingCategory>
|
||||
#include <QPointer>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <memory>
|
||||
|
||||
using namespace std::chrono;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(lcNetworkHttp, "sync.httplogger", QtWarningMsg)
|
||||
|
||||
const qint64 PeekSize = 1024 * 1024;
|
||||
|
||||
bool isTextBody(const QString &s)
|
||||
{
|
||||
static const QRegularExpression regexp(QStringLiteral("^(text/.*?|(application/(xml|.*?json|x-www-form-urlencoded)(;|$)))"));
|
||||
return regexp.match(s).hasMatch();
|
||||
}
|
||||
|
||||
struct HttpContext
|
||||
{
|
||||
HttpContext(const QNetworkRequest &request)
|
||||
: originalUrl(request.url().toString())
|
||||
, lastUrl(request.url())
|
||||
, id(QString::fromUtf8(request.rawHeader(QByteArrayLiteral("X-Request-ID"))))
|
||||
{
|
||||
}
|
||||
|
||||
void addRedirect(const QUrl &url)
|
||||
{
|
||||
lastUrl = url;
|
||||
redirectUrls.append(url.toString());
|
||||
}
|
||||
|
||||
const QString originalUrl;
|
||||
QUrl lastUrl;
|
||||
QStringList redirectUrls;
|
||||
const QString id;
|
||||
|
||||
OCC::Utility::ChronoElapsedTimer timer;
|
||||
bool send = false;
|
||||
bool receivedBeforeSent = false;
|
||||
};
|
||||
|
||||
void logHttp(const QByteArray &verb, HttpContext *ctx, QJsonObject &&header, QIODevice *device, bool cached = false)
|
||||
{
|
||||
static const bool redact = !qEnvironmentVariableIsSet("QSFERA_HTTPLOGGER_NO_REDACT");
|
||||
const auto reply = qobject_cast<QNetworkReply *>(device);
|
||||
const auto contentLength = device ? device->size() : 0;
|
||||
|
||||
if (redact) {
|
||||
const auto authKey = "authorization"_L1;
|
||||
const QString auth = header.value(authKey).toString();
|
||||
if (!auth.isEmpty()) {
|
||||
header.insert(authKey, auth.startsWith(QStringLiteral("Bearer ")) ? QStringLiteral("Bearer [redacted]") : QStringLiteral("Basic [redacted]"));
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject info{{QStringLiteral("method"), QString::fromUtf8(verb)}, {QStringLiteral("id"), ctx->id}, {QStringLiteral("url"), ctx->originalUrl}};
|
||||
|
||||
if (!ctx->redirectUrls.isEmpty()) {
|
||||
info.insert(QStringLiteral("redirects"), QJsonArray::fromStringList(ctx->redirectUrls));
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
// respond
|
||||
QJsonObject replyInfo{{QStringLiteral("status"), reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()},
|
||||
{QStringLiteral("cached"), reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()},
|
||||
// downcast to int, this is json
|
||||
{QStringLiteral("duration"), static_cast<int>(duration_cast<milliseconds>(ctx->timer.duration()).count())}, //
|
||||
{QStringLiteral("durationString"), QDebug::toString(ctx->timer)},
|
||||
{QStringLiteral("version"),
|
||||
QStringLiteral("HTTP %1").arg(
|
||||
reply->attribute(QNetworkRequest::Http2WasUsedAttribute).toBool() ? QStringLiteral("1.1") : QStringLiteral("2"))}};
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
replyInfo.insert(QStringLiteral("error"), reply->errorString());
|
||||
}
|
||||
info.insert(QStringLiteral("reply"), replyInfo);
|
||||
} else {
|
||||
// request was not actually sent but will be returned from cache
|
||||
info.insert(QStringLiteral("cached"), cached);
|
||||
}
|
||||
|
||||
QJsonObject body = {{QStringLiteral("length"), contentLength}};
|
||||
if (contentLength > 0) {
|
||||
const QString contentType = header.value("content-type"_L1).toString();
|
||||
if (isTextBody(contentType)) {
|
||||
if (!device->isOpen()) {
|
||||
Q_ASSERT(dynamic_cast<QBuffer *>(device));
|
||||
// should we close it again?
|
||||
device->open(QIODevice::ReadOnly);
|
||||
}
|
||||
Q_ASSERT(device->pos() == 0);
|
||||
QString data = QString::fromUtf8(device->peek(PeekSize));
|
||||
if (PeekSize < contentLength)
|
||||
{
|
||||
data += QStringLiteral("...(%1 bytes elided)").arg(QString::number(contentLength - PeekSize));
|
||||
}
|
||||
body[QStringLiteral("data")] = data;
|
||||
} else {
|
||||
body[QStringLiteral("data")] = QStringLiteral("%1 bytes of %2 data").arg(QString::number(contentLength), contentType);
|
||||
}
|
||||
}
|
||||
|
||||
qCInfo(lcNetworkHttp).noquote() << (reply ? "RESPONSE" : "REQUEST") << ctx->id
|
||||
<< QJsonDocument{QJsonObject{{reply ? QStringLiteral("response") : QStringLiteral("request"),
|
||||
QJsonObject{{QStringLiteral("info"), info}, {QStringLiteral("header"), header},
|
||||
{QStringLiteral("body"), body}}}}}
|
||||
.toJson(QJsonDocument::Compact);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
void HttpLogger::logRequest(QNetworkReply *reply, QNetworkAccessManager::Operation operation, QIODevice *device)
|
||||
{
|
||||
if (!lcNetworkHttp().isInfoEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto ctx = std::make_unique<HttpContext>(reply->request());
|
||||
|
||||
// device should still exist, lets still use a qpointer to ensure we have valid data
|
||||
const auto logSend = [ctx = ctx.get(), operation, reply, device = QPointer<QIODevice>(device), deviceRaw = device](bool cached = false) {
|
||||
Q_ASSERT(!deviceRaw || device);
|
||||
if (!ctx->send) {
|
||||
ctx->send = true;
|
||||
ctx->timer.reset();
|
||||
} else {
|
||||
// this is a redirect
|
||||
if (ctx->lastUrl != reply->url()) {
|
||||
ctx->addRedirect(reply->url());
|
||||
} else if (ctx->receivedBeforeSent) {
|
||||
qCWarning(lcNetworkHttp) << u"Request:" << ctx->id << u" was sent after we received a result";
|
||||
} else {
|
||||
qCWarning(lcNetworkHttp) << u"Request:" << ctx->id << u" resending";
|
||||
}
|
||||
}
|
||||
|
||||
const auto request = reply->request();
|
||||
QJsonObject header;
|
||||
for (const auto &key : request.rawHeaderList()) {
|
||||
header[QString::fromUtf8(key)] = QString::fromUtf8(request.rawHeader(key));
|
||||
}
|
||||
logHttp(requestVerb(operation, request), ctx, std::move(header), device, cached);
|
||||
};
|
||||
QObject::connect(reply, &QNetworkReply::requestSent, reply, logSend, Qt::DirectConnection);
|
||||
|
||||
|
||||
QObject::connect(
|
||||
reply, &QNetworkReply::finished, reply,
|
||||
[reply, ctx = std::move(ctx), logSend] {
|
||||
if (!ctx->send) {
|
||||
ctx->receivedBeforeSent = true;
|
||||
logSend(reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool());
|
||||
}
|
||||
ctx->timer.stop();
|
||||
|
||||
QJsonObject header;
|
||||
for (const auto &[key, value] : reply->rawHeaderPairs()) {
|
||||
header[QString::fromUtf8(key)] = QString::fromUtf8(value);
|
||||
}
|
||||
logHttp(requestVerb(*reply), ctx.get(), std::move(header), reply);
|
||||
},
|
||||
Qt::DirectConnection);
|
||||
}
|
||||
|
||||
QByteArray HttpLogger::requestVerb(QNetworkAccessManager::Operation operation, const QNetworkRequest &request)
|
||||
{
|
||||
switch (operation) {
|
||||
case QNetworkAccessManager::HeadOperation:
|
||||
return QByteArrayLiteral("HEAD");
|
||||
case QNetworkAccessManager::GetOperation:
|
||||
return QByteArrayLiteral("GET");
|
||||
case QNetworkAccessManager::PutOperation:
|
||||
return QByteArrayLiteral("PUT");
|
||||
case QNetworkAccessManager::PostOperation:
|
||||
return QByteArrayLiteral("POST");
|
||||
case QNetworkAccessManager::DeleteOperation:
|
||||
return QByteArrayLiteral("DELETE");
|
||||
case QNetworkAccessManager::CustomOperation:
|
||||
return request.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray();
|
||||
case QNetworkAccessManager::UnknownOperation:
|
||||
break;
|
||||
}
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user