Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Would you like to add ssh function to edit remote file? Thanks! #20

Open
zhangfq-chemistry opened this issue Jun 18, 2024 · 2 comments
Open
Labels
enhancement New feature or request

Comments

@zhangfq-chemistry
Copy link

No description provided.

@zrax zrax added the enhancement New feature or request label Jun 18, 2024
@zrax
Copy link
Owner

zrax commented Jun 18, 2024

I feel like remote file editing is out of scope for qtextpad, which is designed to be simple and lightweight... But I'll leave this open for discussion

@sandmeteor
Copy link

@zhangfq-chemistry
Assuming you're using Linux along with Qt6/KDE6, you can use the SFTP protocol for file transfer. SFTP is a part of the SSH standard and uses the same connection as SSH. I was able to successfully read and write files in QTextPad by adding new functionality in qtextpadwindow.h and qtextpadwindow.cpp using KDE KIO functions.

  1. Let’s start by making some changes to the CMakeLists.txt file in the root directory of the project.
    To add the necessary KDE Frameworks dependencies for SFTP support, include the following modifications:
    Modified CMakeLists.txt (Main Folder)
# Add include directories for KDE Frameworks
include_directories(
    /usr/include/KF6/KIO
    /usr/include/KF6/KIOWidgets
    /usr/include/KF6/KIOCore
    /usr/include/KF6/KIOGui
    /usr/include/KF6/KService
    /usr/include/KF6/KCoreAddons
)

# Find KDE Frameworks components
find_package(KF6 REQUIRED COMPONENTS CoreAddons KIO)

# Link libraries to the main target
if(NOT QTEXTPAD_WIDGET_ONLY)
    target_link_libraries(qtextpad
        KF6::CoreAddons
        KF6::KIOCore
        KF6::KIOWidgets
    )
endif()

# Link libraries to the library target
target_link_libraries(qtextpadlib
    KF6::CoreAddons
    KF6::KIOCore
    KF6::KIOWidgets
)
  1. Next, to enable opening files from a remote host, replace QFileDialog::getOpenFileName/QFileDialog::getSaveFileName with the following functions:
QList<QUrl> urls = QFileDialog::getOpenFileUrls(this, tr("Open File"), startPath);
downloadFile(urls.at(0));

QUrl saveUrl = QFileDialog::getSaveFileUrl(this, tr("Save File As"), startPath, tr("All Files (*.*)"), nullptr);
if (saveUrl.isEmpty()) {
  return false;
}
if (!saveDocumentToSftp(saveUrl.toString())) {
  return false;
}

This allows accessing files over SFTP by entering the following syntax in the file dialog path:

sftp://[email protected]/home/user/documents/example.txt
  1. Now we’ll add functions to load files from SFTP in the qtextpadwindow.h file. These functions will allow downloading files, processing their content, and decoding the downloaded data. Add the following to your header file:
#include <KIO/StoredTransferJob>
#include <KIO/OpenUrlJob>
#include <QUrl>
#include <QBuffer>

///....

void downloadFile(const QUrl &fileUrl, const QString &textEncoding = QString());
void handleFileContent(const QByteArray &data, const QString &filename, const QString &textEncoding);
QString decodeContent(const QByteArray &data, const QString &textEncoding);

Below is the implementation of the downloadFile, handleFileContent, and decodeContent functions in qtextpadwindow.cpp.


void QTextPadWindow::downloadFile(const QUrl &fileUrl, const QString &textEncoding)
{
	auto *job = KIO::storedGet(fileUrl, KIO::NoReload);
	QProgressDialog *progressDialog = new QProgressDialog(this);
	progressDialog->setWindowTitle(tr("Downloading File"));
	progressDialog->setLabelText(tr("Downloading %1").arg(fileUrl.toString()));
	progressDialog->setRange(0, 100);
	progressDialog->setValue(0);
	progressDialog->setCancelButton(nullptr);
	progressDialog->setModal(true);
	progressDialog->show();

	connect(job, &KIO::Job::percentChanged, this, [progressDialog](KJob *, unsigned long percent) {
		qDebug() << percent;
		progressDialog->setValue(static_cast<int>(percent));
	});
	connect(job, &KJob::result, this, [this, job, progressDialog, fileUrl, textEncoding]() {
		progressDialog->deleteLater();

		if (job->error()) {
			QMessageBox::critical(this, tr("Error"),
									tr("Failed to download file: %1\n%2")
									.arg(job->url().toString(), job->errorString()));
		} else {
			QMessageBox::information(this, tr("Success"),
									 tr("File successfully downloaded to memory."));
			QByteArray data = static_cast<KIO::StoredTransferJob *>(job)->data();
			handleFileContent(data, fileUrl.fileName(), textEncoding);
		}
	});
}


void QTextPadWindow::handleFileContent(const QByteArray &data, const QString &filename, const QString &textEncoding)
{
	QString document = decodeContent(data, textEncoding);
	m_editor->clear();
	m_editor->setPlainText(document);
	m_editor->document()->clearUndoRedoStacks();
	setOpenFilename(filename);
	updateTitle();
}

QString QTextPadWindow::decodeContent(const QByteArray &data, const QString &textEncoding)
{
	TextCodec *codec = nullptr;
	if (!textEncoding.isEmpty()) {
		codec = QTextPadCharsets::codecForName(textEncoding.toLatin1());
	}
	if (!codec) {
		codec = QTextPadCharsets::codecForName("UTF-8");
	}
	return codec->toUnicode(data);
}
  1. It would also be useful to add functionality for saving files via SFTP. However, there is an issue with the Overwrite flag not correctly overwriting the file. As a workaround, the solution involves deleting the file first before saving it.

bool QTextPadWindow::saveDocumentToSftp(const QString &filename)
{
	QUrl fileUrl = QUrl::fromUserInput(filename);

	if (fileUrl.scheme() == "sftp" || fileUrl.scheme() == "ftp" || fileUrl.scheme() == "http") {
		// Delete the file first if it exists
		auto *deleteJob = KIO::file_delete(fileUrl, KIO::HideProgressInfo);
		connect(deleteJob, &KJob::result, this, [this, deleteJob, fileUrl]() {
			if (deleteJob->error() && deleteJob->error() != KIO::ERR_DOES_NOT_EXIST) {
				QMessageBox::critical(this, QObject::tr("Error"),
										QObject::tr("Failed to delete existing file: %1\n%2")
										.arg(fileUrl.toString(), deleteJob->errorString()));
				return;
			}

			// Prepare data for saving
			QByteArray data = m_editor->toPlainText().toUtf8();
			QBuffer *buffer = new QBuffer(this);
			buffer->setData(data);
			buffer->open(QIODevice::ReadOnly);

			// Save the new file
			auto *putJob = KIO::put(fileUrl, KIO::Overwrite | KIO::HideProgressInfo);
			connect(putJob, &KIO::TransferJob::dataReq, this, [buffer](KIO::Job *, QByteArray &output) {
				if (!buffer->atEnd()) {
					output = buffer->read(4096); // Read chunk from buffer
				} else {
					output.clear(); // Signal end of data transfer
				}
			});

			connect(putJob, &KJob::result, this, [this, putJob, buffer, fileUrl]() {
				buffer->deleteLater();
				if (putJob->error()) {
					QMessageBox::critical(this, QObject::tr("Error"),
											QObject::tr("Failed to save file: %1\n%2")
											.arg(fileUrl.toString(), putJob->errorString()));
				} else {
					QMessageBox::information(this, QObject::tr("Success"),
											 QObject::tr("File successfully saved to %1").arg(fileUrl.toString()));

					// Set permissions if needed
					auto *chmodJob = KIO::chmod(fileUrl, 0644);
					connect(chmodJob, &KJob::result, this, [this, chmodJob]() {
						if (chmodJob->error()) {
							QMessageBox::critical(this, QObject::tr("Error"),
													QObject::tr("Failed to set permissions: %1").arg(chmodJob->errorString()));
						}
					});
					chmodJob->start();
				}
			});
		});

		deleteJob->start();
		return true; // Asynchronous operation
	}

	// Handle saving to a local file ..  FIXME
	return saveDocumentAs();
}

Of course, this solution still requires a lot of work, but as a basic starting point, it’s fine. Alternatively, it might be better to implement a Python server for transferring file content using QWebSocket and this approach will be independent of the operating system because the current solution is unlikely to work on Windows, where support for SFTP is not available in getOpenFileUrls. Using a Python WebSocket server for transferring file content can bypass such platform-specific limitations and ensure cross-platform compatibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants