/* * Copyright (C) by Olivier Goffart * Copyright (C) by Klaas Freitag * Copyright (C) by Daniel Heule * * 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 #include #include #include #include #include #include #include #include #include #include #include "account.h" #include "configfile.h" // ONLY ACCESS THE STATIC FUNCTIONS! #ifdef TOKEN_AUTH_ONLY # include "creds/tokencredentials.h" #else # include "creds/httpcredentials.h" #endif #include "simplesslerrorhandler.h" #include "syncengine.h" #include "common/syncjournaldb.h" #include "config.h" #include "csync_exclude.h" #include "theme.h" #include "netrcparser.h" #include "libsync/logger.h" #include "config.h" #ifdef Q_OS_WIN32 #include #else #include #include #endif using namespace OCC; namespace { struct CmdOptions { QString source_dir; QString target_url; QString config_directory; QString user; QString password; QString proxy; bool silent = false; bool trustSSL = false; bool useNetrc = false; bool interactive = true; bool ignoreHiddenFiles = true; QString exclude; QString unsyncedfolders; QString davPath; int restartTimes = 3; int downlimit = 0; int uplimit = 0; bool deltasync; qint64 deltasyncminfilesize; }; struct SyncCTX { explicit SyncCTX(const CmdOptions &cmdOptions) : options { cmdOptions } { } CmdOptions options; QUrl credentialFreeUrl; QString folder; AccountPtr account; QString user; }; /* If the selective sync list is different from before, we need to disable the read from db (The normal client does it in SelectiveSyncDialog::accept*) */ void selectiveSyncFixup(OCC::SyncJournalDb *journal, const QStringList &newList) { SqlDatabase db; if (!db.openOrCreateReadWrite(journal->databaseFilePath())) { return; } bool ok; auto oldBlackListSet = journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok).toSet(); if (ok) { auto blackListSet = newList.toSet(); const auto changes = (oldBlackListSet - blackListSet) + (blackListSet - oldBlackListSet); for (const auto &it : changes) { journal->schedulePathForRemoteDiscovery(it); } journal->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, newList); } } void sync(const SyncCTX &ctx) { QStringList selectiveSyncList; if (!ctx.options.unsyncedfolders.isEmpty()) { QFile f(ctx.options.unsyncedfolders); if (!f.open(QFile::ReadOnly)) { qCritical() << "Could not open file containing the list of unsynced folders: " << ctx.options.unsyncedfolders; } else { // filter out empty lines and comments selectiveSyncList = QString::fromUtf8(f.readAll()).split('\n').filter(QRegExp("\\S+")).filter(QRegExp("^[^#]")); for (int i = 0; i < selectiveSyncList.count(); ++i) { if (!selectiveSyncList.at(i).endsWith(QLatin1Char('/'))) { selectiveSyncList[i].append(QLatin1Char('/')); } } } } const QString dbPath = ctx.options.source_dir + SyncJournalDb::makeDbName(ctx.options.source_dir); auto db = new SyncJournalDb(dbPath, qApp); if (!selectiveSyncList.empty()) { selectiveSyncFixup(db, selectiveSyncList); } SyncOptions opt; opt.fillFromEnvironmentVariables(); opt.verifyChunkSizes(); auto engine = new SyncEngine(ctx.account, ctx.options.source_dir, ctx.folder, db); engine->setParent(db); QObject::connect(engine, &SyncEngine::finished, engine, [engine, ctx, restartCount = std::make_shared(0)](bool result) { if (!result) { qWarning() << "Failed to sync"; qApp->exit(EXIT_FAILURE); } else { if (engine->isAnotherSyncNeeded() != NoFollowUpSync) { if (*restartCount < ctx.options.restartTimes) { (*restartCount)++; qDebug() << "Restarting Sync, because another sync is needed" << *restartCount; engine->startSync(); return; } qWarning() << "Another sync is needed, but not done because restart count is exceeded" << *restartCount; } else { qApp->quit(); } } }); QObject::connect(engine, &SyncEngine::aboutToRemoveAllFiles, engine, [ctx](OCC::SyncFileItem::Direction dir, std::function abort) { if (!ctx.options.interactive) { abort(false); } else { std::cout << (dir == SyncFileItem::Down ? "All files in the sync folder '%1' folder were deleted on the server.\n" "These deletes will be synchronized to your local sync folder, making such files " "unavailable unless you have a right to restore. \n" "If you decide to keep the files, they will be re-synced with the server if you have rights to do so.\n" "If you decide to delete the files, they will be unavailable to you, unless you are the owner." : "All the files in your local sync folder '%1' were deleted. These deletes will be " "synchronized with your server, making such files unavailable unless restored.\n" "Are you sure you want to sync those actions with the server?\n" "If this was an accident and you decide to keep your files, they will be re-synced from the server.") << std::endl; std::string s; while (true) { std::cout << "Remove all files?[y,n]"; std::getline(std::cin, s); if (s == "y") { abort(false); } else if (s == "n") { abort(true); } else { continue; } return; } } }); QObject::connect(engine, &SyncEngine::syncError, engine, [](const QString &error) { qWarning() << "Sync error:" << error; }); engine->setSyncOptions(opt); engine->setIgnoreHiddenFiles(ctx.options.ignoreHiddenFiles); engine->setNetworkLimits(ctx.options.uplimit, ctx.options.downlimit); // Exclude lists bool hasUserExcludeFile = !ctx.options.exclude.isEmpty(); QString systemExcludeFile = ConfigFile::excludeFileFromSystem(); // Always try to load the user-provided exclude list if one is specified if (hasUserExcludeFile) { engine->excludedFiles().addExcludeFilePath(ctx.options.exclude); } // Load the system list if available, or if there's no user-provided list if (!hasUserExcludeFile || QFile::exists(systemExcludeFile)) { engine->excludedFiles().addExcludeFilePath(systemExcludeFile); } if (!engine->excludedFiles().reloadExcludeFiles()) { qFatal("Cannot load system exclude list or list supplied via --exclude"); } engine->startSync(); } } class EchoDisabler { public: EchoDisabler() { #ifdef Q_OS_WIN hStdin = GetStdHandle(STD_INPUT_HANDLE); GetConsoleMode(hStdin, &mode); SetConsoleMode(hStdin, mode & (~ENABLE_ECHO_INPUT)); #else tcgetattr(STDIN_FILENO, &tios); termios tios_new = tios; tios_new.c_lflag &= ~ECHO; tcsetattr(STDIN_FILENO, TCSANOW, &tios_new); #endif } ~EchoDisabler() { #ifdef Q_OS_WIN SetConsoleMode(hStdin, mode); #else tcsetattr(STDIN_FILENO, TCSANOW, &tios); #endif } private: #ifdef Q_OS_WIN DWORD mode = 0; HANDLE hStdin; #else termios tios; #endif }; QString queryPassword(const QString &user) { EchoDisabler disabler; std::cout << "Password for user " << qUtf8Printable(user) << ": "; std::string s; std::getline(std::cin, s); return QString::fromStdString(s); } #ifndef TOKEN_AUTH_ONLY class HttpCredentialsText : public HttpCredentials { public: HttpCredentialsText(const QString &user, const QString &password) : HttpCredentials(DetermineAuthTypeJob::AuthType::Basic, user, password) , _sslTrusted(false) { } void askFromUser() override { _password = ::queryPassword(user()); _ready = true; persist(); emit asked(); } void setSSLTrusted(bool isTrusted) { _sslTrusted = isTrusted; } bool sslIsTrusted() override { return _sslTrusted; } private: bool _sslTrusted; }; #endif /* TOKEN_AUTH_ONLY */ void help() { const char *binaryName = APPLICATION_EXECUTABLE "cmd"; std::cout << binaryName << " - command line " APPLICATION_NAME " client tool" << std::endl; std::cout << "" << std::endl; std::cout << "Usage: " << binaryName << " [OPTION] " << std::endl; std::cout << "" << std::endl; std::cout << "A proxy can either be set manually using --httpproxy." << std::endl; std::cout << "Otherwise, the setting from a configured sync client will be used." << std::endl; std::cout << std::endl; std::cout << "Options:" << std::endl; std::cout << " --silent, -s Don't be so verbose" << std::endl; std::cout << " --httpproxy [proxy] Specify a http proxy to use." << std::endl; std::cout << " Proxy is http://server:port" << std::endl; std::cout << " --trust Trust the SSL certification." << std::endl; std::cout << " --exclude [file] Exclude list file" << std::endl; std::cout << " --unsyncedfolders [file] File containing the list of unsynced remote folders (selective sync)" << std::endl; std::cout << " --user, -u [name] Use [name] as the login name" << std::endl; std::cout << " --password, -p [pass] Use [pass] as password" << std::endl; std::cout << " -n Use netrc (5) for login" << std::endl; std::cout << " --non-interactive Do not block execution with interaction" << std::endl; std::cout << " --davpath [path] Custom themed dav path" << std::endl; std::cout << " --max-sync-retries [n] Retries maximum n times (default to 3)" << std::endl; std::cout << " --uplimit [n] Limit the upload speed of files to n KB/s" << std::endl; std::cout << " --downlimit [n] Limit the download speed of files to n KB/s" << std::endl; std::cout << " -h Sync hidden files,do not ignore them" << std::endl; std::cout << " --version, -v Display version and exit" << std::endl; std::cout << " --logdebug More verbose logging" << std::endl; std::cout << "" << std::endl; exit(0); } void showVersion() { std::cout << qUtf8Printable(Theme::instance()->versionSwitchOutput()); exit(0); } CmdOptions parseOptions(const QStringList &app_args) { CmdOptions options; QStringList args(app_args); int argCount = args.count(); if (argCount < 3) { if (argCount >= 2) { const QString option = args.at(1); if (option == "-v" || option == "--version") { showVersion(); } } help(); } options.target_url = args.takeLast(); // check if the webDAV path was added to the url and append if not. if (!options.target_url.endsWith("/")) { options.target_url.append("/"); } options.source_dir = args.takeLast(); if (!options.source_dir.endsWith('/')) { options.source_dir.append('/'); } QFileInfo fi(options.source_dir); if (!fi.exists()) { std::cerr << "Source dir '" << qUtf8Printable(options.source_dir) << "' does not exist." << std::endl; exit(1); } options.source_dir = fi.absoluteFilePath(); QStringListIterator it(args); // skip file name; if (it.hasNext()) it.next(); while (it.hasNext()) { const QString option = it.next(); if (option == "--httpproxy" && !it.peekNext().startsWith("-")) { options.proxy = it.next(); } else if (option == "-s" || option == "--silent") { options.silent = true; } else if (option == "--trust") { options.trustSSL = true; } else if (option == "-n") { options.useNetrc = true; } else if (option == "-h") { options.ignoreHiddenFiles = false; } else if (option == "--non-interactive") { options.interactive = false; } else if ((option == "-u" || option == "--user") && !it.peekNext().startsWith("-")) { options.user = it.next(); } else if ((option == "-p" || option == "--password") && !it.peekNext().startsWith("-")) { options.password = it.next(); } else if (option == "--exclude" && !it.peekNext().startsWith("-")) { options.exclude = it.next(); } else if (option == "--unsyncedfolders" && !it.peekNext().startsWith("-")) { options.unsyncedfolders = it.next(); } else if (option == "--davpath" && !it.peekNext().startsWith("-")) { options.davPath = it.next(); } else if (option == "--max-sync-retries" && !it.peekNext().startsWith("-")) { options.restartTimes = it.next().toInt(); } else if (option == "--uplimit" && !it.peekNext().startsWith("-")) { options.uplimit = it.next().toInt() * 1000; } else if (option == "--downlimit" && !it.peekNext().startsWith("-")) { options.downlimit = it.next().toInt() * 1000; } else if (option == "--logdebug") { Logger::instance()->setLogFile("-"); Logger::instance()->setLogDebug(true); } else { help(); } } if (options.target_url.isEmpty() || options.source_dir.isEmpty()) { help(); } return options; } int main(int argc, char **argv) { QCoreApplication app(argc, argv); #ifdef Q_OS_WIN // Ensure OpenSSL config file is only loaded from app directory QString opensslConf = QCoreApplication::applicationDirPath() + QString("/openssl.cnf"); qputenv("OPENSSL_CONF", opensslConf.toLocal8Bit()); #endif SyncCTX ctx { parseOptions(app.arguments()) }; if (ctx.options.silent) { qInstallMessageHandler([](QtMsgType, const QMessageLogContext &, const QString &) {}); } else { qSetMessagePattern(Logger::loggerPattern()); } ctx.account = Account::create(); if (!ctx.account) { qFatal("Could not initialize account!"); } if (!ctx.options.davPath.isEmpty()) { ctx.account->setDavPath(ctx.options.davPath); } if (!ctx.options.target_url.contains(ctx.account->davPath())) { ctx.options.target_url.append(ctx.account->davPath()); } const QUrl url = [&ctx] { auto tmp = QUrl::fromUserInput(ctx.options.target_url); // Find the folder and the original owncloud url QStringList splitted = tmp.path().split("/" + ctx.account->davPath()); tmp.setPath(splitted.value(0)); tmp.setScheme(tmp.scheme().replace("owncloud", "http")); // Remote folders typically start with a / and don't end with one ctx.folder = "/" + splitted.value(1); if (ctx.folder.endsWith("/") && ctx.folder != "/") { ctx.folder.chop(1); } return tmp; }(); ctx.credentialFreeUrl = url.adjusted(QUrl::RemoveUserInfo); // Order of retrieval attempt (later attempts override earlier ones): // 1. From URL // 2. From options // 3. From netrc (if enabled) // 4. From prompt (if interactive) ctx.user = url.userName(); QString password = url.password(); if (!ctx.options.user.isEmpty()) { ctx.user = ctx.options.user; } if (!ctx.options.password.isEmpty()) { password = ctx.options.password; } if (ctx.options.useNetrc) { NetrcParser parser; if (parser.parse()) { NetrcParser::LoginPair pair = parser.find(url.host()); ctx.user = pair.first; password = pair.second; } } if (ctx.options.interactive) { if (ctx.user.isEmpty()) { std::cout << "Please enter user name: "; std::string s; std::getline(std::cin, s); ctx.user = QString::fromStdString(s); } if (password.isEmpty()) { password = queryPassword(ctx.user); } } if (!ctx.options.proxy.isNull()) { QString host; int port = 0; bool ok; QStringList pList = ctx.options.proxy.split(':'); if (pList.count() == 3) { // http: //192.168.178.23 : 8080 // 0 1 2 host = pList.at(1); if (host.startsWith("//")) host.remove(0, 2); port = pList.at(2).toInt(&ok); QNetworkProxyFactory::setUseSystemConfiguration(false); QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::HttpProxy, host, port)); } else { qFatal("Could not read httpproxy. The proxy should have the format \"http://hostname:port\"."); } } SimpleSslErrorHandler *sslErrorHandler = new SimpleSslErrorHandler; #ifdef TOKEN_AUTH_ONLY TokenCredentials *cred = new TokenCredentials(ctx.user, password, ""); account->setCredentials(cred); #else HttpCredentialsText *cred = new HttpCredentialsText(ctx.user, password); ctx.account->setCredentials(cred); if (ctx.options.trustSSL) { cred->setSSLTrusted(true); } #endif ctx.account->setUrl(ctx.credentialFreeUrl); ctx.account->setSslErrorHandler(sslErrorHandler); // Perform a call to get the capabilities. auto capabilitiesJob = new JsonApiJob(ctx.account, QLatin1String("ocs/v1.php/cloud/capabilities")); QObject::connect(capabilitiesJob, &JsonApiJob::jsonReceived, qApp, [capabilitiesJob, ctx](const QJsonDocument &json) { auto caps = json.object().value("ocs").toObject().value("data").toObject().value("capabilities").toObject(); qDebug() << "Server capabilities" << caps; ctx.account->setCapabilities(caps.toVariantMap()); ctx.account->setServerVersion(caps["core"].toObject()["status"].toObject()["version"].toString()); if (capabilitiesJob->reply()->error() != QNetworkReply::NoError) { qFatal("Error connecting to server"); } auto userJob = new JsonApiJob(ctx.account, QLatin1String("ocs/v1.php/cloud/user")); QObject::connect(userJob, &JsonApiJob::jsonReceived, qApp, [ctx](const QJsonDocument &json) { const QJsonObject data = json.object().value("ocs").toObject().value("data").toObject(); ctx.account->setDavUser(data.value("id").toString()); ctx.account->setDavDisplayName(data.value("display-name").toString()); // much lower age than the default since this utility is usually made to be run right after a change in the tests SyncEngine::minimumFileAgeForUpload = std::chrono::milliseconds(0); sync(ctx); }); userJob->start(); }); capabilitiesJob->start(); return app.exec(); }