first commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(dolphin-ftp-share-tab)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
set(KF_MIN_VERSION "6.0.0")
|
||||
set(QT_MIN_VERSION "6.6.0")
|
||||
|
||||
find_package(ECM ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
|
||||
include(KDEInstallDirs)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
|
||||
find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS Core Widgets)
|
||||
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS CoreAddons I18n KIO)
|
||||
|
||||
add_subdirectory(src)
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Create build directory
|
||||
mkdir -p build
|
||||
cd build
|
||||
|
||||
# Configure
|
||||
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
|
||||
|
||||
# Build
|
||||
make -j$(nproc)
|
||||
|
||||
echo ""
|
||||
echo "Build successful!"
|
||||
echo "To install, run: cd build && sudo make install"
|
||||
echo "Then restart Dolphin: dolphin --replace &"
|
||||
@@ -0,0 +1,13 @@
|
||||
add_library(ftpshareplugin MODULE
|
||||
ftpshareplugin.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(ftpshareplugin
|
||||
KF6::CoreAddons
|
||||
KF6::I18n
|
||||
KF6::KIOWidgets
|
||||
Qt::Widgets
|
||||
)
|
||||
|
||||
install(TARGETS ftpshareplugin
|
||||
DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf6/propertiesdialog)
|
||||
@@ -0,0 +1,286 @@
|
||||
#include "ftpshareplugin.h"
|
||||
|
||||
#include <KPluginFactory>
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QProcess>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
K_PLUGIN_CLASS_WITH_JSON(FtpSharePlugin, "ftpshareplugin.json")
|
||||
|
||||
// Permission levels for the combo boxes
|
||||
static const QStringList permissionLabels = {
|
||||
QStringLiteral("---"),
|
||||
QStringLiteral("Read Only"),
|
||||
QStringLiteral("Read-Write")
|
||||
};
|
||||
|
||||
// Map combo index to ACL string
|
||||
static QString aclForIndex(int index)
|
||||
{
|
||||
switch (index) {
|
||||
case 1: return QStringLiteral("ro");
|
||||
case 2: return QStringLiteral("rw");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
|
||||
// Map ACL string back to combo index
|
||||
static int indexForAcl(const QString &acl)
|
||||
{
|
||||
if (acl == QLatin1String("ro")) return 1;
|
||||
if (acl == QLatin1String("rw")) return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get list of local users (UID >= 1000, excluding nobody)
|
||||
static QStringList getLocalUsers()
|
||||
{
|
||||
QStringList users;
|
||||
QFile passwd(QStringLiteral("/etc/passwd"));
|
||||
if (passwd.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
while (!passwd.atEnd()) {
|
||||
const QString line = QString::fromLocal8Bit(passwd.readLine()).trimmed();
|
||||
if (line.isEmpty() || line.startsWith(QLatin1Char('#')))
|
||||
continue;
|
||||
const QStringList fields = line.split(QLatin1Char(':'));
|
||||
if (fields.size() < 7)
|
||||
continue;
|
||||
const int uid = fields[2].toInt();
|
||||
const QString shell = fields[6];
|
||||
if (uid >= 1000 && uid < 65534
|
||||
&& !shell.endsWith(QLatin1String("/nologin"))
|
||||
&& !shell.endsWith(QLatin1String("/false"))) {
|
||||
users << fields[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
||||
: KPropertiesDialogPlugin(qobject_cast<KPropertiesDialog *>(parent))
|
||||
{
|
||||
Q_UNUSED(args)
|
||||
|
||||
m_path = properties->item().localPath();
|
||||
if (m_path.isEmpty())
|
||||
return;
|
||||
|
||||
// Default share name = folder name
|
||||
m_defaultShareName = m_path.section(QLatin1Char('/'), -1);
|
||||
|
||||
// --- Build the UI ---
|
||||
m_page = new QWidget();
|
||||
auto *mainLayout = new QVBoxLayout(m_page);
|
||||
|
||||
// Share checkbox
|
||||
m_shareCheckBox = new QCheckBox(
|
||||
i18n("Share this folder via FTP"));
|
||||
mainLayout->addWidget(m_shareCheckBox);
|
||||
|
||||
// Share name
|
||||
auto *nameLayout = new QFormLayout();
|
||||
m_nameEdit = new QLineEdit(m_defaultShareName);
|
||||
nameLayout->addRow(i18n("Name:"), m_nameEdit);
|
||||
mainLayout->addLayout(nameLayout);
|
||||
|
||||
// Anonymous access
|
||||
m_anonymousCheckBox = new QCheckBox(i18n("Allow Anonymous Access"));
|
||||
mainLayout->addWidget(m_anonymousCheckBox);
|
||||
|
||||
// User permissions section
|
||||
auto *usersGroup = new QGroupBox();
|
||||
usersGroup->setFlat(true);
|
||||
auto *usersLayout = new QVBoxLayout(usersGroup);
|
||||
|
||||
// Scroll area for user list
|
||||
auto *scrollArea = new QScrollArea();
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
m_usersWidget = new QWidget();
|
||||
auto *usersFormLayout = new QFormLayout(m_usersWidget);
|
||||
usersFormLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// Add local users
|
||||
const QStringList users = getLocalUsers();
|
||||
for (const QString &user : users) {
|
||||
auto *combo = new QComboBox();
|
||||
combo->addItems(permissionLabels);
|
||||
combo->setCurrentIndex(0); // Default: ---
|
||||
usersFormLayout->addRow(user, combo);
|
||||
m_userPerms.append({user, combo});
|
||||
connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, [this]() { setDirty(true); });
|
||||
}
|
||||
|
||||
scrollArea->setWidget(m_usersWidget);
|
||||
usersLayout->addWidget(scrollArea);
|
||||
mainLayout->addWidget(usersGroup);
|
||||
|
||||
mainLayout->addStretch();
|
||||
|
||||
// Connect signals
|
||||
connect(m_shareCheckBox, &QCheckBox::toggled,
|
||||
this, &FtpSharePlugin::onShareToggled);
|
||||
connect(m_nameEdit, &QLineEdit::textChanged,
|
||||
this, [this]() { setDirty(true); });
|
||||
connect(m_anonymousCheckBox, &QCheckBox::toggled,
|
||||
this, [this]() { setDirty(true); });
|
||||
|
||||
// Load existing share info
|
||||
loadCurrentShare();
|
||||
|
||||
// Initial UI state
|
||||
onShareToggled(m_shareCheckBox->isChecked());
|
||||
|
||||
// Add the page as a tab
|
||||
properties->addPage(m_page, i18n("FTP"));
|
||||
}
|
||||
|
||||
void FtpSharePlugin::onShareToggled(bool checked)
|
||||
{
|
||||
m_nameEdit->setEnabled(checked);
|
||||
m_anonymousCheckBox->setEnabled(checked);
|
||||
m_usersWidget->setEnabled(checked);
|
||||
setDirty(true);
|
||||
}
|
||||
|
||||
void FtpSharePlugin::loadCurrentShare()
|
||||
{
|
||||
// Run: dolphin-ftp-share info
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||
{QStringLiteral("info")});
|
||||
proc.waitForFinished(5000);
|
||||
|
||||
const QString output = QString::fromLocal8Bit(proc.readAllStandardOutput());
|
||||
if (output.isEmpty())
|
||||
return;
|
||||
|
||||
// Parse the output to find a share matching our path
|
||||
// Format:
|
||||
// [sharename]
|
||||
// path=/some/path
|
||||
// anonymous=y
|
||||
// user_acl=user1:rw,user2:ro
|
||||
QString currentSection;
|
||||
QString sharePath;
|
||||
QString shareAcl;
|
||||
QString anonymous;
|
||||
|
||||
const QStringList lines = output.split(QLatin1Char('\n'));
|
||||
for (const QString &line : lines) {
|
||||
if (line.startsWith(QLatin1Char('['))) {
|
||||
// If we had a previous section that matched, break
|
||||
if (!currentSection.isEmpty() && sharePath == m_path)
|
||||
break;
|
||||
|
||||
// New section
|
||||
currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1);
|
||||
sharePath.clear();
|
||||
shareAcl.clear();
|
||||
anonymous.clear();
|
||||
} else if (line.startsWith(QLatin1String("path="))) {
|
||||
sharePath = line.mid(5);
|
||||
} else if (line.startsWith(QLatin1String("user_acl="))) {
|
||||
shareAcl = line.mid(9);
|
||||
} else if (line.startsWith(QLatin1String("anonymous="))) {
|
||||
anonymous = line.mid(10);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if last section matched
|
||||
if (sharePath != m_path)
|
||||
return;
|
||||
|
||||
// Found an existing share!
|
||||
m_isShared = true;
|
||||
m_originalShareName = currentSection;
|
||||
m_shareCheckBox->setChecked(true);
|
||||
m_nameEdit->setText(currentSection);
|
||||
m_anonymousCheckBox->setChecked(anonymous == QLatin1String("y"));
|
||||
|
||||
// Parse ACL: "user1:rw,user2:ro"
|
||||
if (!shareAcl.isEmpty()) {
|
||||
const QStringList aclEntries = shareAcl.split(QLatin1Char(','));
|
||||
for (const QString &entry : aclEntries) {
|
||||
const int colonPos = entry.lastIndexOf(QLatin1Char(':'));
|
||||
if (colonPos < 0)
|
||||
continue;
|
||||
const QString name = entry.left(colonPos);
|
||||
const QString perm = entry.mid(colonPos + 1);
|
||||
|
||||
for (auto &up : m_userPerms) {
|
||||
if (up.username.compare(name, Qt::CaseInsensitive) == 0) {
|
||||
up.combo->setCurrentIndex(indexForAcl(perm));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset dirty state after loading
|
||||
setDirty(false);
|
||||
}
|
||||
|
||||
void FtpSharePlugin::applyChanges()
|
||||
{
|
||||
const bool wantShared = m_shareCheckBox->isChecked();
|
||||
const QString shareName = m_nameEdit->text().trimmed();
|
||||
|
||||
// If unchecked and was previously shared, delete the share
|
||||
if (!wantShared && m_isShared) {
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||
{QStringLiteral("delete"), m_originalShareName});
|
||||
proc.waitForFinished(5000);
|
||||
m_isShared = false;
|
||||
m_originalShareName.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wantShared)
|
||||
return;
|
||||
|
||||
// If name changed, delete old share first
|
||||
if (m_isShared && !m_originalShareName.isEmpty()
|
||||
&& m_originalShareName != shareName) {
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||
{QStringLiteral("delete"), m_originalShareName});
|
||||
proc.waitForFinished(5000);
|
||||
}
|
||||
|
||||
// Build ACL string
|
||||
QStringList aclParts;
|
||||
for (const auto &up : m_userPerms) {
|
||||
const QString acl = aclForIndex(up.combo->currentIndex());
|
||||
if (!acl.isEmpty()) {
|
||||
aclParts << up.username + QLatin1Char(':') + acl;
|
||||
}
|
||||
}
|
||||
|
||||
const QString aclString = aclParts.join(QLatin1Char(','));
|
||||
const QString anonymous = m_anonymousCheckBox->isChecked()
|
||||
? QStringLiteral("y")
|
||||
: QStringLiteral("n");
|
||||
|
||||
// dolphin-ftp-share add <name> <path> <anonymous> <acl>
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||
{QStringLiteral("add"), shareName, m_path, anonymous, aclString});
|
||||
proc.waitForFinished(5000);
|
||||
|
||||
if (proc.exitCode() == 0) {
|
||||
m_isShared = true;
|
||||
m_originalShareName = shareName;
|
||||
}
|
||||
}
|
||||
|
||||
#include "ftpshareplugin.moc"
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef FTPSHAREPLUGIN_H
|
||||
#define FTPSHAREPLUGIN_H
|
||||
|
||||
#include <KPropertiesDialog>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QWidget>
|
||||
|
||||
struct UserPermission {
|
||||
QString username;
|
||||
QComboBox *combo;
|
||||
};
|
||||
|
||||
class FtpSharePlugin : public KPropertiesDialogPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FtpSharePlugin(QObject *parent, const QVariantList &args);
|
||||
void applyChanges() override;
|
||||
|
||||
private:
|
||||
void loadCurrentShare();
|
||||
void onShareToggled(bool checked);
|
||||
|
||||
QString m_path;
|
||||
QString m_defaultShareName;
|
||||
|
||||
QWidget *m_page = nullptr;
|
||||
QCheckBox *m_shareCheckBox = nullptr;
|
||||
QLineEdit *m_nameEdit = nullptr;
|
||||
QCheckBox *m_anonymousCheckBox = nullptr;
|
||||
QWidget *m_usersWidget = nullptr;
|
||||
|
||||
QVector<UserPermission> m_userPerms;
|
||||
|
||||
bool m_isShared = false;
|
||||
QString m_originalShareName;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"KPlugin": {
|
||||
"Name": "FTP Share",
|
||||
"Name[de]": "FTP-Freigabe",
|
||||
"Description": "Configure FTP folder sharing",
|
||||
"Description[de]": "FTP-Ordnerfreigabe konfigurieren",
|
||||
"Icon": "network-server",
|
||||
"MimeTypes": [
|
||||
"inode/directory"
|
||||
]
|
||||
},
|
||||
"X-KDE-Protocols": [
|
||||
"file"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user