You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			487 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			487 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
		
			Executable File
		
	
	
	
	
| #include <stable.h>
 | |
| 
 | |
| #include "cliapplication.h"
 | |
| #include "constants.h"
 | |
| #include "database.h"
 | |
| #include "paths.h"
 | |
| #include "uuid.h"
 | |
| #include "settings.h"
 | |
| #include "models/folder.h"
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| #include <signal.h>
 | |
| 
 | |
| 
 | |
| 
 | |
| namespace jop {
 | |
| 
 | |
| StdoutHandler::StdoutHandler() : QTextStream(stdout) {}
 | |
| StderrHandler::StderrHandler() : QTextStream(stderr) {}
 | |
| 
 | |
| CliApplication::CliApplication(int &argc, char **argv) : QCoreApplication(argc, argv) {
 | |
| 	// This is linked to where the QSettings will be saved. In other words,
 | |
| 	// if these values are changed, the settings will be reset and saved
 | |
| 	// somewhere else.
 | |
| 	QCoreApplication::setOrganizationName(jop::ORG_NAME);
 | |
| 	QCoreApplication::setOrganizationDomain(jop::ORG_DOMAIN);
 | |
| 	QCoreApplication::setApplicationName(jop::APP_NAME);
 | |
| 
 | |
| 	qInfo() << "Config dir:" << paths::configDir();
 | |
| 	qInfo() << "Database file:" << paths::databaseFile();
 | |
| 	qInfo() << "SSL:" << QSslSocket::sslLibraryBuildVersionString() << QSslSocket::sslLibraryVersionNumber();
 | |
| 
 | |
| 	jop::db().initialize(paths::databaseFile());
 | |
| 
 | |
| 	Settings::initialize();
 | |
| 
 | |
| 	Settings settings;
 | |
| 
 | |
| 	if (!settings.contains("clientId")) {
 | |
| 		// Client ID should be unique per instance of a program
 | |
| 		settings.setValue("clientId", uuid::createUuid());
 | |
| 	}
 | |
| 
 | |
| 	connect(&api_, SIGNAL(requestDone(const QJsonObject&, const QString&)), this, SLOT(api_requestDone(const QJsonObject&, const QString&)));
 | |
| 	connect(&synchronizer_, SIGNAL(started()), this, SLOT(synchronizer_started()));
 | |
| 	connect(&synchronizer_, SIGNAL(finished()), this, SLOT(synchronizer_finished()));
 | |
| }
 | |
| 
 | |
| CliApplication::~CliApplication() {
 | |
| 	jop::db().close();
 | |
| }
 | |
| 
 | |
| void CliApplication::api_requestDone(const QJsonObject& response, const QString& tag) {
 | |
| 	// TODO: handle errors
 | |
| 	// Handle expired sessions
 | |
| 
 | |
| 	if (tag == "getSession") {
 | |
| 		if (response.contains("error")) {
 | |
| 			qStderr() << "Could not login: " << response.value("error").toString() << endl;
 | |
| 			emit synchronizationDone();
 | |
| 		} else {
 | |
| 			QString sessionId = response.value("id").toString();
 | |
| 			Settings settings;
 | |
| 			settings.setValue("session.id", sessionId);
 | |
| 			startSynchronization();
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Call this only once the API base URL has been defined and the session has been set.
 | |
| void CliApplication::startSynchronization() {
 | |
| 	Settings settings;
 | |
| 	synchronizer_.api().setBaseUrl(api_.baseUrl());
 | |
| 	synchronizer_.setSessionId(settings.value("session.id").toString());
 | |
| 	synchronizer_.unfreeze();
 | |
| 	synchronizer_.start();
 | |
| }
 | |
| 
 | |
| void CliApplication::synchronizer_started() {
 | |
| 	qDebug() << "Synchronization started...";
 | |
| }
 | |
| 
 | |
| void CliApplication::synchronizer_finished() {
 | |
| 	qDebug() << "Synchronization finished...";
 | |
| 	emit synchronizationDone();
 | |
| }
 | |
| 
 | |
| bool CliApplication::filePutContents(const QString& filePath, const QString& content) const {
 | |
| 	QFile file(filePath);
 | |
| 	if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return false;
 | |
| 
 | |
| 	QTextStream out(&file);
 | |
| 	out << content;
 | |
| 	out.flush();
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| QString CliApplication::fileGetContents(const QString& filePath) const {
 | |
| 	QFile file(filePath);
 | |
| 	if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return QString("");
 | |
| 
 | |
| 	QTextStream in(&file);
 | |
| 	return in.readAll();
 | |
| }
 | |
| 
 | |
| void CliApplication::saveNoteIfFileChanged(Note& note, const QDateTime& originalLastModified, const QString& noteFilePath) {
 | |
| 	if (originalLastModified == QFileInfo(noteFilePath).lastModified()) return;
 | |
| 
 | |
| 	QString content = fileGetContents(noteFilePath);
 | |
| 	if (content.isEmpty()) return;
 | |
| 
 | |
| 	note.patchFriendlyString(content);
 | |
| 	note.save();
 | |
| }
 | |
| 
 | |
| // int CliApplication::execCommandConfig(QCommandLineParser& parser) {
 | |
| // 	parser.addPositionalArgument("key", "Key of the config property.");
 | |
| // 	parser.addPositionalArgument("value", "Value of the config property.");
 | |
| 
 | |
| // 	QCommandLineOption unsetOption(QStringList() << "unset", "Unset the given <key>.", "key");
 | |
| // 	parser.addOption(unsetOption);
 | |
| 
 | |
| // 	QStringList args = parser.positionalArguments();
 | |
| // 	Settings settings;
 | |
| 
 | |
| // 	QString propKey = args.size() >= 1 ? args[0] : "";
 | |
| // 	QString propValue = args.size() >= 2 ? args[1] : "";
 | |
| // 	if (propKey.isEmpty()) {
 | |
| // 		QStringList propKeys = settings.allKeys();
 | |
| // 		for (int i = 0; i < propKeys.size(); i++) {
 | |
| // 			qStdout() << settings.keyValueserialize(propKeys[i]) << endl;
 | |
| // 		}
 | |
| // 		return 0;
 | |
| // 	}
 | |
| 
 | |
| // 	if (propValue.isEmpty()) {
 | |
| // 		qStdout() << settings.keyValueserialize(propKey) << endl;
 | |
| // 		return 0;
 | |
| // 	}
 | |
| 
 | |
| // 	settings.setValue(propKey, propValue);
 | |
| 
 | |
| // 	return 0;
 | |
| // }
 | |
| 
 | |
| QStringList CliApplication::parseCommandLinePath(const QString& commandLine) const {
 | |
| 	QStringList output;
 | |
| 	int state = 0; // 0 = "outside quotes", 1 = "inside quotes"
 | |
| 	QString current("");
 | |
| 	for (int i = 0; i < commandLine.length(); i++) {
 | |
| 		QChar c = commandLine[i];
 | |
| 		
 | |
| 		// End quote
 | |
| 		if (c == '"' && state == 1) {
 | |
| 			output << current;
 | |
| 			current = "";
 | |
| 			state = 0;
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		// Start quote
 | |
| 		if (c == '"' && state == 0) {
 | |
| 			state = 1;
 | |
| 			current = current.trimmed();
 | |
| 			if (current != "") output << current;
 | |
| 			current = "";
 | |
| 			state = 1;
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		// A space when not inside a quoted string
 | |
| 		if (c == ' ' && state == 0) {
 | |
| 			current = current.trimmed();
 | |
| 			if (current != "") output << current;
 | |
| 			current = "";
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		current += c;
 | |
| 	}
 | |
| 
 | |
| 	if (state == 0) current = current.trimmed();
 | |
| 	if (current != "") output << current;
 | |
| 
 | |
| 	return output;
 | |
| }
 | |
| 
 | |
| QString CliApplication::commandLineArgsToString(const QStringList& args) const {
 | |
| 	QString output;
 | |
| 	for (int i = 0; i < args.size(); i++) {
 | |
| 		if (output != "") output += " ";
 | |
| 		QString arg = args[i];
 | |
| 		if (arg.contains(' ')) {
 | |
| 			output += QString("\"%1\"").arg(arg);
 | |
| 		} else {
 | |
| 			output += arg;
 | |
| 		}
 | |
| 	}
 | |
| 	return output;
 | |
| }
 | |
| 
 | |
| int CliApplication::exec() {
 | |
| 	qDebug() << "===========================================";
 | |
| 
 | |
| 	Settings settings;
 | |
| 
 | |
| 	QString command = "help";
 | |
| 	QStringList args = arguments();
 | |
| 
 | |
| 	if (args.size() >= 2) {
 | |
| 		command = args[1];
 | |
| 		args.erase(args.begin() + 1);
 | |
| 	}
 | |
| 
 | |
| 	QCommandLineParser parser;
 | |
| 	QCommandLineOption helpOption(QStringList() << "h" << "help", "Display usage information.");
 | |
| 	parser.addOption(helpOption);
 | |
| 	parser.addVersionOption();
 | |
| 
 | |
| 	// mkdir "new_folder"
 | |
| 	// rm "new_folder"
 | |
| 	// ls
 | |
| 	// ls new_folder
 | |
| 	// touch new_folder/new_note
 | |
| 	// edit new_folder/new_note
 | |
| 	// config editor "subl -w %1"
 | |
| 	// sync
 | |
| 
 | |
| 	// TODO: implement mv "new_folder"
 | |
| 
 | |
| 	if (command == "mkdir") {
 | |
| 		parser.addPositionalArgument("path", "Folder path.");
 | |
| 	} else if (command == "rm") {
 | |
| 		parser.addPositionalArgument("path", "Folder path.");
 | |
| 	} else if (command == "ls") {
 | |
| 		parser.addPositionalArgument("path", "Folder path.");
 | |
| 	} else if (command == "touch") {
 | |
| 		parser.addPositionalArgument("path", "Note path.");
 | |
| 	} else if (command == "edit") {
 | |
| 		parser.addPositionalArgument("path", "Note path.");
 | |
| 	} else if (command == "config") {
 | |
| 		parser.addPositionalArgument("key", "Key of the config property.");
 | |
| 		parser.addPositionalArgument("value", "Value of the config property.");
 | |
| 		parser.addOption(QCommandLineOption(QStringList() << "unset", "Unset the given <key>.", "key"));
 | |
| 	} else if (command == "sync") {
 | |
| 
 | |
| 	} else if (command == "help") {
 | |
| 
 | |
| 	} else {
 | |
| 		qStderr() << parser.helpText() << endl;
 | |
| 		return 1;
 | |
| 	}
 | |
| 
 | |
| 	parser.process(args);
 | |
| 
 | |
| 	if (parser.isSet(helpOption) || command == "help") {
 | |
| 		qStdout() << parser.helpText();
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	args = parser.positionalArguments();
 | |
| 
 | |
| 	int errorCode = 0;
 | |
| 
 | |
| 	if (command == "mkdir") {
 | |
| 		QString path = args.size() ? args[0] : QString();
 | |
| 
 | |
| 		if (path.isEmpty()) {
 | |
| 			qStderr() << "Please provide a path or name for the folder.";
 | |
| 			return 1;
 | |
| 		}
 | |
| 
 | |
| 		std::vector<std::unique_ptr<Folder>> folders = Folder::pathToFolders(path, false, errorCode);
 | |
| 		if (errorCode) {
 | |
| 			qStderr() << "Invalid path: " << path << endl;
 | |
| 			return 1;
 | |
| 		}
 | |
| 
 | |
| 		Folder folder;
 | |
| 		folder.setValue("parent_id", folders.size() ? folders[folders.size() - 1]->idString() : "");
 | |
| 		folder.setValue("title", Folder::pathBaseName(path));
 | |
| 		folder.save();
 | |
| 	}
 | |
| 
 | |
| 	if (command == "rm") {
 | |
| 		QString path = args.size() ? args[0] : QString();
 | |
| 
 | |
| 		if (path.isEmpty()) {
 | |
| 			qStderr() << "Please provide a path or name for the folder.";
 | |
| 			return 1;
 | |
| 		}
 | |
| 
 | |
| 		std::vector<std::unique_ptr<Folder>> folders = Folder::pathToFolders(path, true, errorCode);
 | |
| 		if (errorCode || !folders.size()) {
 | |
| 			qStderr() << "Invalid path: " << path << endl;
 | |
| 			return 1;
 | |
| 		}
 | |
| 
 | |
| 		folders[folders.size() - 1]->dispose();
 | |
| 	}
 | |
| 
 | |
| 	if (command == "ls") {
 | |
| 		QString path = args.size() ? args[0] : QString();
 | |
| 		std::vector<std::unique_ptr<Folder>> folders = Folder::pathToFolders(path, true, errorCode);
 | |
| 
 | |
| 		if (errorCode) {
 | |
| 			qStderr() << "Invalid path: " << path << endl;
 | |
| 			return 1;
 | |
| 		}
 | |
| 
 | |
| 		std::vector<std::unique_ptr<BaseModel>> children;
 | |
| 		if (folders.size()) {
 | |
| 			children = folders[folders.size() - 1]->children();
 | |
| 		} else {
 | |
| 			std::unique_ptr<Folder> root = Folder::root();
 | |
| 			children = root->children();
 | |
| 		}
 | |
| 
 | |
| 		qStdout() << QString("Total: %1 items").arg(children.size()) << endl;
 | |
| 		for (size_t i = 0; i < children.size(); i++) {
 | |
| 			qStdout() << children[i]->displayTitle() << endl;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (command == "touch") {
 | |
| 		QString path = args.size() ? args[0] : QString();
 | |
| 
 | |
| 		if (path.isEmpty()) {
 | |
| 			qStderr() << "Please provide a path or name for the note.";
 | |
| 			return 1;
 | |
| 		}
 | |
| 
 | |
| 		std::vector<std::unique_ptr<Folder>> folders = Folder::pathToFolders(path, false, errorCode);
 | |
| 
 | |
| 		if (errorCode) {
 | |
| 			qStderr() << "Invalid path: " << path << endl;
 | |
| 		} else {
 | |
| 			QString noteTitle = Folder::pathBaseName(path);
 | |
| 
 | |
| 			Note note;
 | |
| 			note.setValue("parent_id", folders.size() ? folders[folders.size() - 1]->idString() : "");
 | |
| 			note.setValue("title", noteTitle);
 | |
| 			note.save();
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (command == "edit") {
 | |
| 		QString path = args.size() ? args[0] : QString();
 | |
| 
 | |
| 		if (path.isEmpty()) {
 | |
| 			qStderr() << "Please provide a path or name for the note.";
 | |
| 			return 1;
 | |
| 		}
 | |
| 
 | |
| 		std::vector<std::unique_ptr<Folder>> folders = Folder::pathToFolders(path, false, errorCode);
 | |
| 
 | |
| 		if (errorCode) {
 | |
| 			qStderr() << "Invalid path: " << path << endl;
 | |
| 		} else {
 | |
| 			// TODO: handle case where two notes with the same title exist
 | |
| 
 | |
| 			QString editorCommandString = settings.value("editor").toString().trimmed();
 | |
| 			if (editorCommandString.isEmpty()) {
 | |
| 				qStderr() << "No editor is defined. Please define one using the \"config editor\" command." << endl;
 | |
| 				return 1;
 | |
| 			}
 | |
| 
 | |
| 			QStringList editorCommand = parseCommandLinePath(editorCommandString);
 | |
| 
 | |
| 			QString parentId = folders.size() ? folders[folders.size() - 1]->idString() : QString("");
 | |
| 			QString noteTitle = Folder::pathBaseName(path);
 | |
| 			Note note;
 | |
| 			if (!note.loadByField(parentId, QString("title"), noteTitle)) {
 | |
| 				note.setValue("parent_id", folders.size() ? folders[folders.size() - 1]->idString() : "");
 | |
| 				note.setValue("title", noteTitle);
 | |
| 				note.save();
 | |
| 				note.reload(); // To ensure that all fields are populated with the default values
 | |
| 			}
 | |
| 
 | |
| 			QString noteFilePath = QString("%1/%2.txt").arg(paths::noteDraftsDir()).arg(note.idString());
 | |
| 
 | |
| 			if (!filePutContents(noteFilePath, note.serialize())) {
 | |
| 				qStderr() << QString("Cannot open %1 for writing").arg(noteFilePath) << endl;
 | |
| 				return 1;
 | |
| 			}
 | |
| 
 | |
| 			QFileInfo fileInfo(noteFilePath);
 | |
| 			QDateTime originalLastModified = fileInfo.lastModified();
 | |
| 
 | |
| 			qStdout() << QString("Editing note \"%1\" (Either close the editor or press Ctrl+C when done)").arg(path) << endl;
 | |
| 			qDebug() << "File:" << noteFilePath;
 | |
| 			QProcess* process = new QProcess();
 | |
| 			qint64 processId = 0;
 | |
| 
 | |
| 			QString editorCommandPath = editorCommand.takeFirst();
 | |
| 			editorCommand << noteFilePath;
 | |
| 			if (!process->startDetached(editorCommandPath, editorCommand, QString(), &processId)) {
 | |
| 				qStderr() << QString("Could not start command: %1").arg(editorCommandPath + " " + commandLineArgsToString(editorCommand)) << endl;
 | |
| 				return 1;
 | |
| 			}
 | |
| 
 | |
| 			while (kill(processId, 0) == 0) { // While the process still exist
 | |
| 				QThread::sleep(2);
 | |
| 				saveNoteIfFileChanged(note, originalLastModified, noteFilePath);
 | |
| 			}
 | |
| 
 | |
| 			saveNoteIfFileChanged(note, originalLastModified, noteFilePath);
 | |
| 
 | |
| 			delete process; process = NULL;
 | |
| 
 | |
| 			QFile::remove(noteFilePath);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (command == "config") {
 | |
| 		if (parser.isSet("unset")) {
 | |
| 			QString key = parser.value("unset").trimmed();
 | |
| 			settings.remove(key);
 | |
| 			return 0;
 | |
| 		}
 | |
| 
 | |
| 		QString propKey = args.size() >= 1 ? args[0] : "";
 | |
| 		QString propValue = args.size() >= 2 ? args[1] : "";
 | |
| 		if (propKey.isEmpty()) {
 | |
| 			QStringList propKeys = settings.allKeys();
 | |
| 			for (int i = 0; i < propKeys.size(); i++) {
 | |
| 				qStdout() << settings.keyValueserialize(propKeys[i]) << endl;
 | |
| 			}
 | |
| 			return 0;
 | |
| 		}
 | |
| 
 | |
| 		if (propValue.isEmpty()) {
 | |
| 			qStdout() << settings.keyValueserialize(propKey) << endl;
 | |
| 			return 0;
 | |
| 		}
 | |
| 
 | |
| 		settings.setValue(propKey, propValue);
 | |
| 	}
 | |
| 
 | |
| 	if (command == "sync") {
 | |
| 		QString sessionId = settings.value("session.id").toString();
 | |
| 		qDebug() << "Session ID:" << sessionId;
 | |
| 
 | |
| 		// TODO: ask user
 | |
| 		api_.setBaseUrl("http://127.0.0.1:8000");
 | |
| 
 | |
| 		QEventLoop loop;
 | |
| 		connect(this, SIGNAL(synchronizationDone()), &loop, SLOT(quit()));
 | |
| 
 | |
| 		if (sessionId == "") {
 | |
| 			QTextStream qtin(stdin); 
 | |
| 			qStdout() << "Enter email:" << endl;
 | |
| 			QString email = qtin.readLine();
 | |
| 			qStdout() << "Enter password:" << endl;
 | |
| 			QString password = qtin.readLine();
 | |
| 
 | |
| 			qDebug() << email << password;
 | |
| 
 | |
| 			Settings settings;
 | |
| 			QUrlQuery postData;
 | |
| 			postData.addQueryItem("email", email);
 | |
| 			postData.addQueryItem("password", password);
 | |
| 			postData.addQueryItem("client_id", settings.value("clientId").toString());
 | |
| 			api_.post("sessions", QUrlQuery(), postData, "getSession");
 | |
| 		} else {
 | |
| 			startSynchronization();
 | |
| 		}
 | |
| 
 | |
| 		loop.exec();
 | |
| 
 | |
| 		qDebug() << "Synchronization done";
 | |
| 	}
 | |
| 
 | |
| 	qDebug() << "=========================================== END";
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| }
 |