Umbau auf Pure-FTPd mit PureDB Virtual Users und Passwort-Dialog
- Komplett auf Pure-FTPd umgestellt (weg von vsftpd) - PureDB virtuelle Benutzer: jeder User sieht nur seine Freigaben - Per-User Bind-Mounts mit Read-Only/Read-Write Durchsetzung - Passwort-Dialog beim Anlegen neuer FTP-Benutzer - Change-Password UI: Dropdown + Button zum Passwort ändern - Setup-Kommando für automatische Pure-FTPd Einrichtung - Anonymous-Checkbox entfernt (nur authentifizierte User) - sudoers-Fix: $SUDO_USER statt $USER bei sudo-Ausführung - openssl passwd statt pure-pw stdin für GUI-Kompatibilität Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
34a94f0d82
commit
326d8e97ac
124
README.md
124
README.md
|
|
@ -9,80 +9,63 @@ Ein KDE Dolphin Properties-Dialog Plugin zum Erstellen und Verwalten von FTP-Fre
|
|||
- **FTP-Tab** in Dolphins Ordner-Eigenschaften (Rechtsklick → Eigenschaften)
|
||||
- Freigabe per Checkbox aktivieren/deaktivieren
|
||||
- Freigabename konfigurierbar (Standard = Ordnername)
|
||||
- Anonymen Zugang erlauben
|
||||
- Berechtigungen pro Benutzer: Read Only / Read-Write
|
||||
- Nutzt das Helper-Script `dolphin-ftp-share` — **kein Root nötig!**
|
||||
- Erkennt bestehende Freigaben und lädt deren Einstellungen
|
||||
- **Per-User Berechtigungen**: Read Only / Read-Write pro Benutzer
|
||||
- Jeder Benutzer sieht **nur seine freigegebenen Ordner** per FTP
|
||||
- Read-Only wird direkt am Dateisystem durchgesetzt (read-only Bind-Mount)
|
||||
- Automatische Einrichtung mit `dolphin-ftp-share setup`
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Ein FTP-Server (z.B. `vsftpd`)
|
||||
- Pure-FTPd als FTP-Server
|
||||
- Das Helper-Script `dolphin-ftp-share` im PATH
|
||||
- Build-Dependencies (siehe unten)
|
||||
|
||||
### FTP-Server installieren (vsftpd)
|
||||
## Einrichtung
|
||||
|
||||
### 1. Pure-FTPd installieren
|
||||
|
||||
```bash
|
||||
sudo apt install vsftpd
|
||||
sudo apt install pure-ftpd
|
||||
```
|
||||
|
||||
### vsftpd konfigurieren
|
||||
|
||||
In `/etc/vsftpd.conf`:
|
||||
|
||||
```ini
|
||||
listen=YES
|
||||
anonymous_enable=YES
|
||||
local_enable=YES
|
||||
write_enable=YES
|
||||
anon_root=/home/DEIN_USER/.local/share/dolphin-ftp-root
|
||||
local_root=/home/DEIN_USER/.local/share/dolphin-ftp-root
|
||||
chroot_local_user=YES
|
||||
allow_writeable_chroot=YES
|
||||
pasv_enable=NO
|
||||
port_enable=YES
|
||||
connect_from_port_20=YES
|
||||
|
||||
# Logging
|
||||
xferlog_enable=YES
|
||||
xferlog_std_format=NO
|
||||
log_ftp_protocol=YES
|
||||
vsftpd_log_file=/var/log/vsftpd.log
|
||||
```
|
||||
|
||||
Danach: `sudo systemctl restart vsftpd`
|
||||
|
||||
### Logdatei
|
||||
|
||||
vsftpd schreibt alle FTP-Aktivitäten (Logins, Uploads, Downloads, Fehler) in `/var/log/vsftpd.log`.
|
||||
|
||||
```bash
|
||||
# Logs live verfolgen:
|
||||
sudo tail -f /var/log/vsftpd.log
|
||||
```
|
||||
|
||||
### Helper-Script installieren
|
||||
### 2. Helper-Script installieren
|
||||
|
||||
```bash
|
||||
sudo cp scripts/dolphin-ftp-share /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/dolphin-ftp-share
|
||||
```
|
||||
|
||||
### sudo für Bind-Mounts erlauben
|
||||
### 3. Automatisches Setup
|
||||
|
||||
Das Helper-Script nutzt `sudo mount --bind` und `sudo umount`, um Ordner in das FTP-Root einzubinden (Symlinks funktionieren nicht in vsftpd's chroot-Umgebung).
|
||||
|
||||
Damit das ohne Passwort-Eingabe funktioniert:
|
||||
Das Setup konfiguriert Pure-FTPd komplett und erstellt die sudoers-Regel:
|
||||
|
||||
```bash
|
||||
sudo visudo -f /etc/sudoers.d/dolphin-ftp-share
|
||||
sudo dolphin-ftp-share setup
|
||||
```
|
||||
|
||||
Folgendes eintragen (DEIN_USER ersetzen):
|
||||
Das macht automatisch:
|
||||
- Aktiviert PureDB-Authentifizierung (virtuelle FTP-Benutzer)
|
||||
- Deaktiviert System-Login und anonymen Zugang
|
||||
- Aktiviert Chroot (jeder sieht nur seine Ordner)
|
||||
- Erstellt sudoers-Regel für mount/umount/pure-pw
|
||||
- Aktiviert Logging
|
||||
- Startet Pure-FTPd neu
|
||||
|
||||
### 4. FTP-Passwörter setzen
|
||||
|
||||
FTP-Benutzer werden beim ersten Freigeben automatisch erstellt (Standard-Passwort = Benutzername). Passwort ändern:
|
||||
|
||||
```bash
|
||||
dolphin-ftp-share passwd <benutzername> <neues-passwort>
|
||||
```
|
||||
DEIN_USER ALL=(root) NOPASSWD: /usr/bin/mount --bind *
|
||||
DEIN_USER ALL=(root) NOPASSWD: /usr/bin/umount /home/DEIN_USER/.local/share/dolphin-ftp-root/*
|
||||
|
||||
### Logging
|
||||
|
||||
Pure-FTPd loggt über syslog:
|
||||
|
||||
```bash
|
||||
# Logs live verfolgen:
|
||||
sudo tail -f /var/log/syslog | grep pure-ftpd
|
||||
```
|
||||
|
||||
## Versionen
|
||||
|
|
@ -165,8 +148,12 @@ sudo rm /usr/lib/x86_64-linux-gnu/qt5/plugins/kf5/propertiesdialog/ftpshareplugi
|
|||
# KF6
|
||||
sudo rm /usr/lib/x86_64-linux-gnu/qt6/plugins/kf6/propertiesdialog/ftpshareplugin.so
|
||||
|
||||
# Helper-Script
|
||||
# Helper-Script & sudoers
|
||||
sudo rm /usr/local/bin/dolphin-ftp-share
|
||||
sudo rm /etc/sudoers.d/dolphin-ftp-share
|
||||
|
||||
# Pure-FTPd (optional)
|
||||
sudo apt remove pure-ftpd
|
||||
```
|
||||
|
||||
## Projektstruktur
|
||||
|
|
@ -196,28 +183,31 @@ kde-dolphin-ftp-sharing-tab/
|
|||
|
||||
## Wie es funktioniert
|
||||
|
||||
Das Plugin nutzt das Helper-Script `dolphin-ftp-share`:
|
||||
1. **Rechtsklick auf Ordner → Eigenschaften → FTP-Tab**
|
||||
2. Freigabe aktivieren, Berechtigungen pro Benutzer setzen (Read Only / Read-Write)
|
||||
3. Beim Anwenden ruft das Plugin `dolphin-ftp-share add` auf
|
||||
4. Das Helper-Script:
|
||||
- Erstellt für jeden Benutzer ein persönliches FTP-Root (`~/.local/share/dolphin-ftp-root/users/<name>/`)
|
||||
- Bind-mounted den freigegebenen Ordner dorthin
|
||||
- Read-Only Freigaben werden als read-only Bind-Mount gemountet
|
||||
- Erstellt automatisch virtuelle Pure-FTPd-Benutzer (PureDB)
|
||||
5. **Jeder FTP-Benutzer sieht nur seine freigegebenen Ordner** — durch Chroot auf sein persönliches FTP-Root
|
||||
|
||||
- `dolphin-ftp-share add <name> <path> <anonymous> <acl>` — Freigabe erstellen
|
||||
- `dolphin-ftp-share delete <name>` — Freigabe löschen
|
||||
- `dolphin-ftp-share info` — bestehende Freigaben auflisten
|
||||
### Kommandos
|
||||
|
||||
Das Script speichert Freigabe-Konfigurationen in `~/.local/share/dolphin-ftp-shares/` und erstellt Bind-Mounts in `~/.local/share/dolphin-ftp-root/`. Der FTP-Server (z.B. vsftpd) dient dieses Root-Verzeichnis aus.
|
||||
|
||||
### Konfigurationsformat
|
||||
|
||||
Jede Freigabe wird als `.conf`-Datei gespeichert:
|
||||
|
||||
```ini
|
||||
path=/pfad/zum/ordner
|
||||
anonymous=y
|
||||
user_acl=user1:rw,user2:ro
|
||||
```
|
||||
| Kommando | Beschreibung |
|
||||
|---|---|
|
||||
| `dolphin-ftp-share setup` | Einmalige Pure-FTPd Einrichtung |
|
||||
| `dolphin-ftp-share add <name> <path> <acl>` | Freigabe erstellen |
|
||||
| `dolphin-ftp-share delete <name>` | Freigabe löschen |
|
||||
| `dolphin-ftp-share info` | Bestehende Freigaben auflisten |
|
||||
| `dolphin-ftp-share passwd <user> <pass>` | FTP-Passwort ändern |
|
||||
|
||||
### Berechtigungen
|
||||
|
||||
| Level | Beschreibung |
|
||||
|---|---|
|
||||
| `---` | Kein Zugriff — Ordner ist für diesen Benutzer nicht sichtbar |
|
||||
| `ro` | Read Only — nur Download |
|
||||
| `rw` | Read-Write — Upload und Download |
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
|
@ -37,6 +39,26 @@ static int indexForAcl(const QString &acl)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Check if a virtual FTP user exists
|
||||
static bool ftpUserExists(const QString &username)
|
||||
{
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||
{QStringLiteral("userexists"), username});
|
||||
proc.waitForFinished(5000);
|
||||
return proc.exitCode() == 0;
|
||||
}
|
||||
|
||||
// Create a virtual FTP user with password
|
||||
static bool createFtpUser(const QString &username, const QString &password)
|
||||
{
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||
{QStringLiteral("passwd"), username, password});
|
||||
proc.waitForFinished(5000);
|
||||
return proc.exitCode() == 0;
|
||||
}
|
||||
|
||||
// Get list of local users (UID >= 1000, excluding nobody)
|
||||
static QStringList getLocalUsers()
|
||||
{
|
||||
|
|
@ -89,13 +111,8 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
|||
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 *usersGroup = new QGroupBox(i18n("User Permissions"));
|
||||
auto *usersLayout = new QVBoxLayout(usersGroup);
|
||||
|
||||
// Scroll area for user list
|
||||
|
|
@ -123,6 +140,18 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
|||
usersLayout->addWidget(scrollArea);
|
||||
mainLayout->addWidget(usersGroup);
|
||||
|
||||
// Change password section
|
||||
auto *pwGroup = new QGroupBox(i18n("FTP Password"));
|
||||
auto *pwLayout = new QHBoxLayout(pwGroup);
|
||||
m_pwUserCombo = new QComboBox();
|
||||
m_pwUserCombo->addItems(users);
|
||||
auto *pwButton = new QPushButton(i18n("Change Password..."));
|
||||
pwLayout->addWidget(m_pwUserCombo);
|
||||
pwLayout->addWidget(pwButton);
|
||||
mainLayout->addWidget(pwGroup);
|
||||
|
||||
connect(pwButton, &QPushButton::clicked, this, &FtpSharePlugin::onChangePassword);
|
||||
|
||||
mainLayout->addStretch();
|
||||
|
||||
// Connect signals
|
||||
|
|
@ -130,8 +159,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
|||
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();
|
||||
|
|
@ -146,7 +173,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
|||
void FtpSharePlugin::onShareToggled(bool checked)
|
||||
{
|
||||
m_nameEdit->setEnabled(checked);
|
||||
m_anonymousCheckBox->setEnabled(checked);
|
||||
m_usersWidget->setEnabled(checked);
|
||||
setDirty(true);
|
||||
}
|
||||
|
|
@ -167,12 +193,10 @@ void FtpSharePlugin::loadCurrentShare()
|
|||
// 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) {
|
||||
|
|
@ -185,13 +209,10 @@ void FtpSharePlugin::loadCurrentShare()
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +225,6 @@ void FtpSharePlugin::loadCurrentShare()
|
|||
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()) {
|
||||
|
|
@ -257,6 +277,41 @@ void FtpSharePlugin::applyChanges()
|
|||
proc.waitForFinished(5000);
|
||||
}
|
||||
|
||||
// Create FTP users for users that have permissions but no FTP account yet
|
||||
for (const auto &up : m_userPerms) {
|
||||
if (up.combo->currentIndex() == 0)
|
||||
continue; // No permission set, skip
|
||||
|
||||
if (ftpUserExists(up.username))
|
||||
continue; // Already has FTP account
|
||||
|
||||
// Ask for FTP password
|
||||
bool ok = false;
|
||||
const QString password = QInputDialog::getText(
|
||||
m_page,
|
||||
i18n("FTP Password"),
|
||||
i18n("Set FTP password for user '%1':", up.username),
|
||||
QLineEdit::Password,
|
||||
QString(),
|
||||
&ok);
|
||||
|
||||
if (!ok || password.isEmpty()) {
|
||||
QMessageBox::warning(m_page,
|
||||
i18n("FTP Share"),
|
||||
i18n("No password set for '%1'. This user will be skipped.", up.username));
|
||||
up.combo->setCurrentIndex(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!createFtpUser(up.username, password)) {
|
||||
QMessageBox::warning(m_page,
|
||||
i18n("FTP Share"),
|
||||
i18n("Failed to create FTP user '%1'.", up.username));
|
||||
up.combo->setCurrentIndex(0);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Build ACL string
|
||||
QStringList aclParts;
|
||||
for (const auto &up : m_userPerms) {
|
||||
|
|
@ -267,14 +322,11 @@ void FtpSharePlugin::applyChanges()
|
|||
}
|
||||
|
||||
const QString aclString = aclParts.join(QLatin1Char(','));
|
||||
const QString anonymous = m_anonymousCheckBox->isChecked()
|
||||
? QStringLiteral("y")
|
||||
: QStringLiteral("n");
|
||||
|
||||
// dolphin-ftp-share add <name> <path> <anonymous> <acl>
|
||||
// dolphin-ftp-share add <name> <path> <acl>
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||
{QStringLiteral("add"), shareName, m_path, anonymous, aclString});
|
||||
{QStringLiteral("add"), shareName, m_path, aclString});
|
||||
proc.waitForFinished(5000);
|
||||
|
||||
if (proc.exitCode() == 0) {
|
||||
|
|
@ -283,4 +335,40 @@ void FtpSharePlugin::applyChanges()
|
|||
}
|
||||
}
|
||||
|
||||
void FtpSharePlugin::onChangePassword()
|
||||
{
|
||||
const QString username = m_pwUserCombo->currentText();
|
||||
if (username.isEmpty())
|
||||
return;
|
||||
|
||||
if (!ftpUserExists(username)) {
|
||||
QMessageBox::information(m_page,
|
||||
i18n("FTP Password"),
|
||||
i18n("User '%1' has no FTP account yet.\nSet permissions for this user first.", username));
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const QString password = QInputDialog::getText(
|
||||
m_page,
|
||||
i18n("FTP Password"),
|
||||
i18n("New FTP password for '%1':", username),
|
||||
QLineEdit::Password,
|
||||
QString(),
|
||||
&ok);
|
||||
|
||||
if (!ok || password.isEmpty())
|
||||
return;
|
||||
|
||||
if (createFtpUser(username, password)) {
|
||||
QMessageBox::information(m_page,
|
||||
i18n("FTP Password"),
|
||||
i18n("Password for '%1' updated.", username));
|
||||
} else {
|
||||
QMessageBox::warning(m_page,
|
||||
i18n("FTP Password"),
|
||||
i18n("Failed to update password for '%1'.", username));
|
||||
}
|
||||
}
|
||||
|
||||
#include "ftpshareplugin.moc"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QWidget>
|
||||
|
||||
struct UserPermission {
|
||||
|
|
@ -22,6 +23,7 @@ public:
|
|||
private:
|
||||
void loadCurrentShare();
|
||||
void onShareToggled(bool checked);
|
||||
void onChangePassword();
|
||||
|
||||
QString m_path;
|
||||
QString m_defaultShareName;
|
||||
|
|
@ -29,8 +31,8 @@ private:
|
|||
QWidget *m_page = nullptr;
|
||||
QCheckBox *m_shareCheckBox = nullptr;
|
||||
QLineEdit *m_nameEdit = nullptr;
|
||||
QCheckBox *m_anonymousCheckBox = nullptr;
|
||||
QWidget *m_usersWidget = nullptr;
|
||||
QComboBox *m_pwUserCombo = nullptr;
|
||||
|
||||
QVector<UserPermission> m_userPerms;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
|
@ -37,6 +39,26 @@ static int indexForAcl(const QString &acl)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Check if a virtual FTP user exists
|
||||
static bool ftpUserExists(const QString &username)
|
||||
{
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||
{QStringLiteral("userexists"), username});
|
||||
proc.waitForFinished(5000);
|
||||
return proc.exitCode() == 0;
|
||||
}
|
||||
|
||||
// Create a virtual FTP user with password
|
||||
static bool createFtpUser(const QString &username, const QString &password)
|
||||
{
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||
{QStringLiteral("passwd"), username, password});
|
||||
proc.waitForFinished(5000);
|
||||
return proc.exitCode() == 0;
|
||||
}
|
||||
|
||||
// Get list of local users (UID >= 1000, excluding nobody)
|
||||
static QStringList getLocalUsers()
|
||||
{
|
||||
|
|
@ -89,13 +111,8 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
|||
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 *usersGroup = new QGroupBox(i18n("User Permissions"));
|
||||
auto *usersLayout = new QVBoxLayout(usersGroup);
|
||||
|
||||
// Scroll area for user list
|
||||
|
|
@ -123,6 +140,18 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
|||
usersLayout->addWidget(scrollArea);
|
||||
mainLayout->addWidget(usersGroup);
|
||||
|
||||
// Change password section
|
||||
auto *pwGroup = new QGroupBox(i18n("FTP Password"));
|
||||
auto *pwLayout = new QHBoxLayout(pwGroup);
|
||||
m_pwUserCombo = new QComboBox();
|
||||
m_pwUserCombo->addItems(users);
|
||||
auto *pwButton = new QPushButton(i18n("Change Password..."));
|
||||
pwLayout->addWidget(m_pwUserCombo);
|
||||
pwLayout->addWidget(pwButton);
|
||||
mainLayout->addWidget(pwGroup);
|
||||
|
||||
connect(pwButton, &QPushButton::clicked, this, &FtpSharePlugin::onChangePassword);
|
||||
|
||||
mainLayout->addStretch();
|
||||
|
||||
// Connect signals
|
||||
|
|
@ -130,8 +159,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
|||
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();
|
||||
|
|
@ -146,7 +173,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
|||
void FtpSharePlugin::onShareToggled(bool checked)
|
||||
{
|
||||
m_nameEdit->setEnabled(checked);
|
||||
m_anonymousCheckBox->setEnabled(checked);
|
||||
m_usersWidget->setEnabled(checked);
|
||||
setDirty(true);
|
||||
}
|
||||
|
|
@ -167,12 +193,10 @@ void FtpSharePlugin::loadCurrentShare()
|
|||
// 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) {
|
||||
|
|
@ -185,13 +209,10 @@ void FtpSharePlugin::loadCurrentShare()
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +225,6 @@ void FtpSharePlugin::loadCurrentShare()
|
|||
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()) {
|
||||
|
|
@ -257,6 +277,41 @@ void FtpSharePlugin::applyChanges()
|
|||
proc.waitForFinished(5000);
|
||||
}
|
||||
|
||||
// Create FTP users for users that have permissions but no FTP account yet
|
||||
for (const auto &up : m_userPerms) {
|
||||
if (up.combo->currentIndex() == 0)
|
||||
continue; // No permission set, skip
|
||||
|
||||
if (ftpUserExists(up.username))
|
||||
continue; // Already has FTP account
|
||||
|
||||
// Ask for FTP password
|
||||
bool ok = false;
|
||||
const QString password = QInputDialog::getText(
|
||||
m_page,
|
||||
i18n("FTP Password"),
|
||||
i18n("Set FTP password for user '%1':", up.username),
|
||||
QLineEdit::Password,
|
||||
QString(),
|
||||
&ok);
|
||||
|
||||
if (!ok || password.isEmpty()) {
|
||||
QMessageBox::warning(m_page,
|
||||
i18n("FTP Share"),
|
||||
i18n("No password set for '%1'. This user will be skipped.", up.username));
|
||||
up.combo->setCurrentIndex(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!createFtpUser(up.username, password)) {
|
||||
QMessageBox::warning(m_page,
|
||||
i18n("FTP Share"),
|
||||
i18n("Failed to create FTP user '%1'.", up.username));
|
||||
up.combo->setCurrentIndex(0);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Build ACL string
|
||||
QStringList aclParts;
|
||||
for (const auto &up : m_userPerms) {
|
||||
|
|
@ -267,14 +322,11 @@ void FtpSharePlugin::applyChanges()
|
|||
}
|
||||
|
||||
const QString aclString = aclParts.join(QLatin1Char(','));
|
||||
const QString anonymous = m_anonymousCheckBox->isChecked()
|
||||
? QStringLiteral("y")
|
||||
: QStringLiteral("n");
|
||||
|
||||
// dolphin-ftp-share add <name> <path> <anonymous> <acl>
|
||||
// dolphin-ftp-share add <name> <path> <acl>
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||
{QStringLiteral("add"), shareName, m_path, anonymous, aclString});
|
||||
{QStringLiteral("add"), shareName, m_path, aclString});
|
||||
proc.waitForFinished(5000);
|
||||
|
||||
if (proc.exitCode() == 0) {
|
||||
|
|
@ -283,4 +335,40 @@ void FtpSharePlugin::applyChanges()
|
|||
}
|
||||
}
|
||||
|
||||
void FtpSharePlugin::onChangePassword()
|
||||
{
|
||||
const QString username = m_pwUserCombo->currentText();
|
||||
if (username.isEmpty())
|
||||
return;
|
||||
|
||||
if (!ftpUserExists(username)) {
|
||||
QMessageBox::information(m_page,
|
||||
i18n("FTP Password"),
|
||||
i18n("User '%1' has no FTP account yet.\nSet permissions for this user first.", username));
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const QString password = QInputDialog::getText(
|
||||
m_page,
|
||||
i18n("FTP Password"),
|
||||
i18n("New FTP password for '%1':", username),
|
||||
QLineEdit::Password,
|
||||
QString(),
|
||||
&ok);
|
||||
|
||||
if (!ok || password.isEmpty())
|
||||
return;
|
||||
|
||||
if (createFtpUser(username, password)) {
|
||||
QMessageBox::information(m_page,
|
||||
i18n("FTP Password"),
|
||||
i18n("Password for '%1' updated.", username));
|
||||
} else {
|
||||
QMessageBox::warning(m_page,
|
||||
i18n("FTP Password"),
|
||||
i18n("Failed to update password for '%1'.", username));
|
||||
}
|
||||
}
|
||||
|
||||
#include "ftpshareplugin.moc"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QWidget>
|
||||
|
||||
struct UserPermission {
|
||||
|
|
@ -22,6 +23,7 @@ public:
|
|||
private:
|
||||
void loadCurrentShare();
|
||||
void onShareToggled(bool checked);
|
||||
void onChangePassword();
|
||||
|
||||
QString m_path;
|
||||
QString m_defaultShareName;
|
||||
|
|
@ -29,8 +31,8 @@ private:
|
|||
QWidget *m_page = nullptr;
|
||||
QCheckBox *m_shareCheckBox = nullptr;
|
||||
QLineEdit *m_nameEdit = nullptr;
|
||||
QCheckBox *m_anonymousCheckBox = nullptr;
|
||||
QWidget *m_usersWidget = nullptr;
|
||||
QComboBox *m_pwUserCombo = nullptr;
|
||||
|
||||
QVector<UserPermission> m_userPerms;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,108 @@
|
|||
#!/bin/bash
|
||||
# dolphin-ftp-share - Manage FTP folder shares for Dolphin
|
||||
# dolphin-ftp-share - Manage FTP folder shares for Dolphin with Pure-FTPd
|
||||
#
|
||||
# Each user gets a personal FTP root with only their shared folders.
|
||||
# Virtual FTP users (PureDB) are created automatically.
|
||||
# Permissions (ro/rw) are enforced via bind mount options.
|
||||
#
|
||||
# Usage:
|
||||
# dolphin-ftp-share setup Initial Pure-FTPd setup
|
||||
# dolphin-ftp-share info List all shares
|
||||
# dolphin-ftp-share add <name> <path> <anon> <acl> Add/update a share
|
||||
# dolphin-ftp-share add <name> <path> <acl> Add/update a share
|
||||
# dolphin-ftp-share delete <name> Remove a share
|
||||
#
|
||||
# Share configs are stored in ~/.local/share/dolphin-ftp-shares/
|
||||
# Shares are bind-mounted into ~/.local/share/dolphin-ftp-root/
|
||||
#
|
||||
# The FTP root directory can be served by any FTP server (e.g. vsftpd).
|
||||
# Each share appears as a real subdirectory under the FTP root via bind mount.
|
||||
# dolphin-ftp-share passwd <username> <password> Set FTP password
|
||||
|
||||
set -e
|
||||
|
||||
SHARE_DIR="${HOME}/.local/share/dolphin-ftp-shares"
|
||||
FTP_ROOT="${HOME}/.local/share/dolphin-ftp-root"
|
||||
PASSWD_FILE="/etc/pure-ftpd/pureftpd.passwd"
|
||||
PDB_FILE="/etc/pure-ftpd/pureftpd.pdb"
|
||||
|
||||
CALLER_UID=$(id -u)
|
||||
CALLER_GID=$(id -g)
|
||||
|
||||
# Update home directory for an existing virtual FTP user.
|
||||
ensure_virtual_user() {
|
||||
local username="$1"
|
||||
local user_root="$FTP_ROOT/users/$username"
|
||||
|
||||
mkdir -p "$user_root"
|
||||
|
||||
if sudo pure-pw show "$username" -f "$PASSWD_FILE" >/dev/null 2>&1; then
|
||||
sudo pure-pw usermod "$username" \
|
||||
-d "$user_root" \
|
||||
-f "$PASSWD_FILE" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
rebuild_db() {
|
||||
sudo pure-pw mkdb "$PDB_FILE" -f "$PASSWD_FILE"
|
||||
}
|
||||
|
||||
# Unmount a share from all user roots
|
||||
unmount_share() {
|
||||
local name="$1"
|
||||
if [ -d "$FTP_ROOT/users" ]; then
|
||||
for user_dir in "$FTP_ROOT/users"/*/; do
|
||||
[ -d "$user_dir" ] || continue
|
||||
local mp="${user_dir}${name}"
|
||||
if mountpoint -q "$mp" 2>/dev/null; then
|
||||
sudo umount "$mp"
|
||||
fi
|
||||
rmdir "$mp" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_setup() {
|
||||
echo "Setting up Pure-FTPd for Dolphin FTP sharing..."
|
||||
|
||||
mkdir -p "$FTP_ROOT/users"
|
||||
|
||||
# Enable PureDB authentication
|
||||
echo "$PDB_FILE" | sudo tee /etc/pure-ftpd/conf/PureDB >/dev/null
|
||||
sudo ln -sf /etc/pure-ftpd/conf/PureDB /etc/pure-ftpd/auth/50pure
|
||||
|
||||
# Chroot all users to their FTP home
|
||||
echo "yes" | sudo tee /etc/pure-ftpd/conf/ChrootEveryone >/dev/null
|
||||
|
||||
# Disable anonymous access — only authenticated virtual users
|
||||
echo "yes" | sudo tee /etc/pure-ftpd/conf/NoAnonymous >/dev/null
|
||||
|
||||
# Remove system auth so only PureDB users can log in
|
||||
sudo rm -f /etc/pure-ftpd/auth/65unix /etc/pure-ftpd/auth/70pam
|
||||
|
||||
# Create empty password DB if needed
|
||||
sudo touch "$PASSWD_FILE"
|
||||
sudo pure-pw mkdb "$PDB_FILE" -f "$PASSWD_FILE"
|
||||
|
||||
# Enable logging
|
||||
echo "yes" | sudo tee /etc/pure-ftpd/conf/VerboseLog >/dev/null
|
||||
|
||||
# Create sudoers rule for passwordless mount/umount/pure-pw
|
||||
# Use SUDO_USER to get the real user (not root) when run with sudo
|
||||
local REAL_USER="${SUDO_USER:-$USER}"
|
||||
local REAL_HOME
|
||||
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
|
||||
local REAL_FTP_ROOT="${REAL_HOME}/.local/share/dolphin-ftp-root"
|
||||
|
||||
sudo tee /etc/sudoers.d/dolphin-ftp-share >/dev/null <<EOF
|
||||
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/mount --bind *
|
||||
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/mount -o remount?ro?bind *
|
||||
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/umount ${REAL_FTP_ROOT}/*
|
||||
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/pure-pw *
|
||||
EOF
|
||||
sudo chmod 440 /etc/sudoers.d/dolphin-ftp-share
|
||||
|
||||
sudo systemctl restart pure-ftpd
|
||||
|
||||
echo ""
|
||||
echo "Setup complete!"
|
||||
echo "Share folders via Dolphin: right-click folder → Properties → FTP tab"
|
||||
echo "Default FTP password = username. Change with:"
|
||||
echo " dolphin-ftp-share passwd <user> <newpassword>"
|
||||
}
|
||||
|
||||
cmd_info() {
|
||||
[ -d "$SHARE_DIR" ] || exit 0
|
||||
|
|
@ -31,33 +118,49 @@ cmd_info() {
|
|||
cmd_add() {
|
||||
local name="$1"
|
||||
local path="$2"
|
||||
local anon="$3"
|
||||
local acl="$4"
|
||||
local acl="$3"
|
||||
|
||||
if [ -z "$name" ] || [ -z "$path" ]; then
|
||||
echo "Error: name and path are required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$SHARE_DIR" "$FTP_ROOT"
|
||||
mkdir -p "$SHARE_DIR" "$FTP_ROOT/users"
|
||||
|
||||
# Write share config
|
||||
# Unmount any existing mounts for this share (handles permission changes)
|
||||
unmount_share "$name"
|
||||
|
||||
# Save config
|
||||
cat > "$SHARE_DIR/$name.conf" <<EOF
|
||||
path=$path
|
||||
anonymous=$anon
|
||||
user_acl=$acl
|
||||
EOF
|
||||
|
||||
# Create mount point directory
|
||||
mkdir -p "$FTP_ROOT/$name"
|
||||
# Process per-user ACL: "user1:ro,user2:rw"
|
||||
if [ -n "$acl" ]; then
|
||||
IFS=',' read -ra entries <<< "$acl"
|
||||
for entry in "${entries[@]}"; do
|
||||
local username="${entry%%:*}"
|
||||
local perm="${entry##*:}"
|
||||
local user_root="$FTP_ROOT/users/$username"
|
||||
local mount_point="$user_root/$name"
|
||||
|
||||
# Unmount if already mounted
|
||||
mountpoint -q "$FTP_ROOT/$name" 2>/dev/null && sudo umount "$FTP_ROOT/$name" 2>/dev/null || true
|
||||
ensure_virtual_user "$username"
|
||||
|
||||
# Bind-mount the shared folder into the FTP root
|
||||
sudo mount --bind "$path" "$FTP_ROOT/$name"
|
||||
mkdir -p "$mount_point"
|
||||
|
||||
echo "Share '$name' added: $path -> $FTP_ROOT/$name"
|
||||
# Bind mount the shared folder into user's personal FTP root
|
||||
sudo mount --bind "$path" "$mount_point"
|
||||
|
||||
# Read-only: remount with ro flag
|
||||
if [ "$perm" = "ro" ]; then
|
||||
sudo mount -o remount,ro,bind "$mount_point"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
rebuild_db
|
||||
echo "Share '$name' added: $path"
|
||||
}
|
||||
|
||||
cmd_delete() {
|
||||
|
|
@ -68,32 +171,77 @@ cmd_delete() {
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Unmount bind mount
|
||||
if mountpoint -q "$FTP_ROOT/$name" 2>/dev/null; then
|
||||
sudo umount "$FTP_ROOT/$name"
|
||||
fi
|
||||
|
||||
unmount_share "$name"
|
||||
rm -f "$SHARE_DIR/$name.conf"
|
||||
rmdir "$FTP_ROOT/$name" 2>/dev/null || true
|
||||
|
||||
# Clean up empty directories
|
||||
# Clean up
|
||||
rmdir "$SHARE_DIR" 2>/dev/null || true
|
||||
rmdir "$FTP_ROOT" 2>/dev/null || true
|
||||
|
||||
echo "Share '$name' deleted"
|
||||
}
|
||||
|
||||
cmd_userexists() {
|
||||
local username="$1"
|
||||
|
||||
if [ -z "$username" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if sudo pure-pw show "$username" -f "$PASSWD_FILE" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_passwd() {
|
||||
local username="$1"
|
||||
local password="$2"
|
||||
|
||||
if [ -z "$username" ] || [ -z "$password" ]; then
|
||||
echo "Error: username and password are required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local user_root="$FTP_ROOT/users/$username"
|
||||
mkdir -p "$user_root"
|
||||
|
||||
# Generate MD5 password hash
|
||||
local hash
|
||||
hash=$(openssl passwd -1 "$password")
|
||||
|
||||
if sudo pure-pw show "$username" -f "$PASSWD_FILE" >/dev/null 2>&1; then
|
||||
# User exists — update password in-place
|
||||
sudo sed -i "s|^${username}:[^:]*:|${username}:${hash}:|" "$PASSWD_FILE"
|
||||
echo "Password updated for '$username'"
|
||||
else
|
||||
# User doesn't exist — add entry to passwd file
|
||||
# Format: username:hash:uid:gid:gecos:home:uploadbw:downloadbw:uploadratio:downloadratio:maxconn:filesquota:sizequota:allowedlocalip:allowedclientip:timerestrictions
|
||||
echo "${username}:${hash}:${CALLER_UID}:${CALLER_GID}::${user_root}::::::::::::" | \
|
||||
sudo tee -a "$PASSWD_FILE" >/dev/null
|
||||
echo "Created FTP user '$username'"
|
||||
fi
|
||||
|
||||
rebuild_db
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
info) cmd_info ;;
|
||||
add) cmd_add "$2" "$3" "${4:-n}" "${5:-}" ;;
|
||||
delete) cmd_delete "$2" ;;
|
||||
setup) cmd_setup ;;
|
||||
info) cmd_info ;;
|
||||
add) cmd_add "$2" "$3" "${4:-}" ;;
|
||||
delete) cmd_delete "$2" ;;
|
||||
userexists) cmd_userexists "$2" ;;
|
||||
passwd) cmd_passwd "$2" "$3" ;;
|
||||
*)
|
||||
echo "Usage: dolphin-ftp-share {info|add|delete}" >&2
|
||||
echo "Usage: dolphin-ftp-share {setup|info|add|delete|userexists|passwd}" >&2
|
||||
echo "" >&2
|
||||
echo "Commands:" >&2
|
||||
echo " setup Initial Pure-FTPd setup" >&2
|
||||
echo " info List all shares" >&2
|
||||
echo " add <name> <path> <anon> <acl> Add/update a share" >&2
|
||||
echo " add <name> <path> <acl> Add/update a share" >&2
|
||||
echo " delete <name> Remove a share" >&2
|
||||
echo " userexists <username> Check if FTP user exists" >&2
|
||||
echo " passwd <username> <password> Set/create FTP password" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
Loading…
Reference in New Issue