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)
|
- **FTP-Tab** in Dolphins Ordner-Eigenschaften (Rechtsklick → Eigenschaften)
|
||||||
- Freigabe per Checkbox aktivieren/deaktivieren
|
- Freigabe per Checkbox aktivieren/deaktivieren
|
||||||
- Freigabename konfigurierbar (Standard = Ordnername)
|
- Freigabename konfigurierbar (Standard = Ordnername)
|
||||||
- Anonymen Zugang erlauben
|
- **Per-User Berechtigungen**: Read Only / Read-Write pro Benutzer
|
||||||
- Berechtigungen pro Benutzer: Read Only / Read-Write
|
- Jeder Benutzer sieht **nur seine freigegebenen Ordner** per FTP
|
||||||
- Nutzt das Helper-Script `dolphin-ftp-share` — **kein Root nötig!**
|
- Read-Only wird direkt am Dateisystem durchgesetzt (read-only Bind-Mount)
|
||||||
- Erkennt bestehende Freigaben und lädt deren Einstellungen
|
- Automatische Einrichtung mit `dolphin-ftp-share setup`
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Ein FTP-Server (z.B. `vsftpd`)
|
- Pure-FTPd als FTP-Server
|
||||||
- Das Helper-Script `dolphin-ftp-share` im PATH
|
- Das Helper-Script `dolphin-ftp-share` im PATH
|
||||||
- Build-Dependencies (siehe unten)
|
- Build-Dependencies (siehe unten)
|
||||||
|
|
||||||
### FTP-Server installieren (vsftpd)
|
## Einrichtung
|
||||||
|
|
||||||
|
### 1. Pure-FTPd installieren
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install vsftpd
|
sudo apt install pure-ftpd
|
||||||
```
|
```
|
||||||
|
|
||||||
### vsftpd konfigurieren
|
### 2. Helper-Script installieren
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo cp scripts/dolphin-ftp-share /usr/local/bin/
|
sudo cp scripts/dolphin-ftp-share /usr/local/bin/
|
||||||
sudo chmod +x /usr/local/bin/dolphin-ftp-share
|
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).
|
Das Setup konfiguriert Pure-FTPd komplett und erstellt die sudoers-Regel:
|
||||||
|
|
||||||
Damit das ohne Passwort-Eingabe funktioniert:
|
|
||||||
|
|
||||||
```bash
|
```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
|
## Versionen
|
||||||
|
|
@ -165,8 +148,12 @@ sudo rm /usr/lib/x86_64-linux-gnu/qt5/plugins/kf5/propertiesdialog/ftpshareplugi
|
||||||
# KF6
|
# KF6
|
||||||
sudo rm /usr/lib/x86_64-linux-gnu/qt6/plugins/kf6/propertiesdialog/ftpshareplugin.so
|
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 /usr/local/bin/dolphin-ftp-share
|
||||||
|
sudo rm /etc/sudoers.d/dolphin-ftp-share
|
||||||
|
|
||||||
|
# Pure-FTPd (optional)
|
||||||
|
sudo apt remove pure-ftpd
|
||||||
```
|
```
|
||||||
|
|
||||||
## Projektstruktur
|
## Projektstruktur
|
||||||
|
|
@ -196,28 +183,31 @@ kde-dolphin-ftp-sharing-tab/
|
||||||
|
|
||||||
## Wie es funktioniert
|
## 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
|
### Kommandos
|
||||||
- `dolphin-ftp-share delete <name>` — Freigabe löschen
|
|
||||||
- `dolphin-ftp-share info` — bestehende Freigaben auflisten
|
|
||||||
|
|
||||||
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.
|
| Kommando | Beschreibung |
|
||||||
|
|---|---|
|
||||||
### Konfigurationsformat
|
| `dolphin-ftp-share setup` | Einmalige Pure-FTPd Einrichtung |
|
||||||
|
| `dolphin-ftp-share add <name> <path> <acl>` | Freigabe erstellen |
|
||||||
Jede Freigabe wird als `.conf`-Datei gespeichert:
|
| `dolphin-ftp-share delete <name>` | Freigabe löschen |
|
||||||
|
| `dolphin-ftp-share info` | Bestehende Freigaben auflisten |
|
||||||
```ini
|
| `dolphin-ftp-share passwd <user> <pass>` | FTP-Passwort ändern |
|
||||||
path=/pfad/zum/ordner
|
|
||||||
anonymous=y
|
|
||||||
user_acl=user1:rw,user2:ro
|
|
||||||
```
|
|
||||||
|
|
||||||
### Berechtigungen
|
### Berechtigungen
|
||||||
|
|
||||||
| Level | Beschreibung |
|
| Level | Beschreibung |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| `---` | Kein Zugriff — Ordner ist für diesen Benutzer nicht sichtbar |
|
||||||
| `ro` | Read Only — nur Download |
|
| `ro` | Read Only — nur Download |
|
||||||
| `rw` | Read-Write — Upload und Download |
|
| `rw` | Read-Write — Upload und Download |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@
|
||||||
|
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
|
#include <QInputDialog>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
@ -37,6 +39,26 @@ static int indexForAcl(const QString &acl)
|
||||||
return 0;
|
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)
|
// Get list of local users (UID >= 1000, excluding nobody)
|
||||||
static QStringList getLocalUsers()
|
static QStringList getLocalUsers()
|
||||||
{
|
{
|
||||||
|
|
@ -89,13 +111,8 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
||||||
nameLayout->addRow(i18n("Name:"), m_nameEdit);
|
nameLayout->addRow(i18n("Name:"), m_nameEdit);
|
||||||
mainLayout->addLayout(nameLayout);
|
mainLayout->addLayout(nameLayout);
|
||||||
|
|
||||||
// Anonymous access
|
|
||||||
m_anonymousCheckBox = new QCheckBox(i18n("Allow Anonymous Access"));
|
|
||||||
mainLayout->addWidget(m_anonymousCheckBox);
|
|
||||||
|
|
||||||
// User permissions section
|
// User permissions section
|
||||||
auto *usersGroup = new QGroupBox();
|
auto *usersGroup = new QGroupBox(i18n("User Permissions"));
|
||||||
usersGroup->setFlat(true);
|
|
||||||
auto *usersLayout = new QVBoxLayout(usersGroup);
|
auto *usersLayout = new QVBoxLayout(usersGroup);
|
||||||
|
|
||||||
// Scroll area for user list
|
// Scroll area for user list
|
||||||
|
|
@ -123,6 +140,18 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
||||||
usersLayout->addWidget(scrollArea);
|
usersLayout->addWidget(scrollArea);
|
||||||
mainLayout->addWidget(usersGroup);
|
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();
|
mainLayout->addStretch();
|
||||||
|
|
||||||
// Connect signals
|
// Connect signals
|
||||||
|
|
@ -130,8 +159,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
||||||
this, &FtpSharePlugin::onShareToggled);
|
this, &FtpSharePlugin::onShareToggled);
|
||||||
connect(m_nameEdit, &QLineEdit::textChanged,
|
connect(m_nameEdit, &QLineEdit::textChanged,
|
||||||
this, [this]() { setDirty(true); });
|
this, [this]() { setDirty(true); });
|
||||||
connect(m_anonymousCheckBox, &QCheckBox::toggled,
|
|
||||||
this, [this]() { setDirty(true); });
|
|
||||||
|
|
||||||
// Load existing share info
|
// Load existing share info
|
||||||
loadCurrentShare();
|
loadCurrentShare();
|
||||||
|
|
@ -146,7 +173,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
||||||
void FtpSharePlugin::onShareToggled(bool checked)
|
void FtpSharePlugin::onShareToggled(bool checked)
|
||||||
{
|
{
|
||||||
m_nameEdit->setEnabled(checked);
|
m_nameEdit->setEnabled(checked);
|
||||||
m_anonymousCheckBox->setEnabled(checked);
|
|
||||||
m_usersWidget->setEnabled(checked);
|
m_usersWidget->setEnabled(checked);
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
}
|
}
|
||||||
|
|
@ -167,12 +193,10 @@ void FtpSharePlugin::loadCurrentShare()
|
||||||
// Format:
|
// Format:
|
||||||
// [sharename]
|
// [sharename]
|
||||||
// path=/some/path
|
// path=/some/path
|
||||||
// anonymous=y
|
|
||||||
// user_acl=user1:rw,user2:ro
|
// user_acl=user1:rw,user2:ro
|
||||||
QString currentSection;
|
QString currentSection;
|
||||||
QString sharePath;
|
QString sharePath;
|
||||||
QString shareAcl;
|
QString shareAcl;
|
||||||
QString anonymous;
|
|
||||||
|
|
||||||
const QStringList lines = output.split(QLatin1Char('\n'));
|
const QStringList lines = output.split(QLatin1Char('\n'));
|
||||||
for (const QString &line : lines) {
|
for (const QString &line : lines) {
|
||||||
|
|
@ -185,13 +209,10 @@ void FtpSharePlugin::loadCurrentShare()
|
||||||
currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1);
|
currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1);
|
||||||
sharePath.clear();
|
sharePath.clear();
|
||||||
shareAcl.clear();
|
shareAcl.clear();
|
||||||
anonymous.clear();
|
|
||||||
} else if (line.startsWith(QLatin1String("path="))) {
|
} else if (line.startsWith(QLatin1String("path="))) {
|
||||||
sharePath = line.mid(5);
|
sharePath = line.mid(5);
|
||||||
} else if (line.startsWith(QLatin1String("user_acl="))) {
|
} else if (line.startsWith(QLatin1String("user_acl="))) {
|
||||||
shareAcl = line.mid(9);
|
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_originalShareName = currentSection;
|
||||||
m_shareCheckBox->setChecked(true);
|
m_shareCheckBox->setChecked(true);
|
||||||
m_nameEdit->setText(currentSection);
|
m_nameEdit->setText(currentSection);
|
||||||
m_anonymousCheckBox->setChecked(anonymous == QLatin1String("y"));
|
|
||||||
|
|
||||||
// Parse ACL: "user1:rw,user2:ro"
|
// Parse ACL: "user1:rw,user2:ro"
|
||||||
if (!shareAcl.isEmpty()) {
|
if (!shareAcl.isEmpty()) {
|
||||||
|
|
@ -257,6 +277,41 @@ void FtpSharePlugin::applyChanges()
|
||||||
proc.waitForFinished(5000);
|
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
|
// Build ACL string
|
||||||
QStringList aclParts;
|
QStringList aclParts;
|
||||||
for (const auto &up : m_userPerms) {
|
for (const auto &up : m_userPerms) {
|
||||||
|
|
@ -267,14 +322,11 @@ void FtpSharePlugin::applyChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString aclString = aclParts.join(QLatin1Char(','));
|
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;
|
QProcess proc;
|
||||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||||
{QStringLiteral("add"), shareName, m_path, anonymous, aclString});
|
{QStringLiteral("add"), shareName, m_path, aclString});
|
||||||
proc.waitForFinished(5000);
|
proc.waitForFinished(5000);
|
||||||
|
|
||||||
if (proc.exitCode() == 0) {
|
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"
|
#include "ftpshareplugin.moc"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
struct UserPermission {
|
struct UserPermission {
|
||||||
|
|
@ -22,6 +23,7 @@ public:
|
||||||
private:
|
private:
|
||||||
void loadCurrentShare();
|
void loadCurrentShare();
|
||||||
void onShareToggled(bool checked);
|
void onShareToggled(bool checked);
|
||||||
|
void onChangePassword();
|
||||||
|
|
||||||
QString m_path;
|
QString m_path;
|
||||||
QString m_defaultShareName;
|
QString m_defaultShareName;
|
||||||
|
|
@ -29,8 +31,8 @@ private:
|
||||||
QWidget *m_page = nullptr;
|
QWidget *m_page = nullptr;
|
||||||
QCheckBox *m_shareCheckBox = nullptr;
|
QCheckBox *m_shareCheckBox = nullptr;
|
||||||
QLineEdit *m_nameEdit = nullptr;
|
QLineEdit *m_nameEdit = nullptr;
|
||||||
QCheckBox *m_anonymousCheckBox = nullptr;
|
|
||||||
QWidget *m_usersWidget = nullptr;
|
QWidget *m_usersWidget = nullptr;
|
||||||
|
QComboBox *m_pwUserCombo = nullptr;
|
||||||
|
|
||||||
QVector<UserPermission> m_userPerms;
|
QVector<UserPermission> m_userPerms;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@
|
||||||
|
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
|
#include <QInputDialog>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
@ -37,6 +39,26 @@ static int indexForAcl(const QString &acl)
|
||||||
return 0;
|
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)
|
// Get list of local users (UID >= 1000, excluding nobody)
|
||||||
static QStringList getLocalUsers()
|
static QStringList getLocalUsers()
|
||||||
{
|
{
|
||||||
|
|
@ -89,13 +111,8 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
||||||
nameLayout->addRow(i18n("Name:"), m_nameEdit);
|
nameLayout->addRow(i18n("Name:"), m_nameEdit);
|
||||||
mainLayout->addLayout(nameLayout);
|
mainLayout->addLayout(nameLayout);
|
||||||
|
|
||||||
// Anonymous access
|
|
||||||
m_anonymousCheckBox = new QCheckBox(i18n("Allow Anonymous Access"));
|
|
||||||
mainLayout->addWidget(m_anonymousCheckBox);
|
|
||||||
|
|
||||||
// User permissions section
|
// User permissions section
|
||||||
auto *usersGroup = new QGroupBox();
|
auto *usersGroup = new QGroupBox(i18n("User Permissions"));
|
||||||
usersGroup->setFlat(true);
|
|
||||||
auto *usersLayout = new QVBoxLayout(usersGroup);
|
auto *usersLayout = new QVBoxLayout(usersGroup);
|
||||||
|
|
||||||
// Scroll area for user list
|
// Scroll area for user list
|
||||||
|
|
@ -123,6 +140,18 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
||||||
usersLayout->addWidget(scrollArea);
|
usersLayout->addWidget(scrollArea);
|
||||||
mainLayout->addWidget(usersGroup);
|
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();
|
mainLayout->addStretch();
|
||||||
|
|
||||||
// Connect signals
|
// Connect signals
|
||||||
|
|
@ -130,8 +159,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
||||||
this, &FtpSharePlugin::onShareToggled);
|
this, &FtpSharePlugin::onShareToggled);
|
||||||
connect(m_nameEdit, &QLineEdit::textChanged,
|
connect(m_nameEdit, &QLineEdit::textChanged,
|
||||||
this, [this]() { setDirty(true); });
|
this, [this]() { setDirty(true); });
|
||||||
connect(m_anonymousCheckBox, &QCheckBox::toggled,
|
|
||||||
this, [this]() { setDirty(true); });
|
|
||||||
|
|
||||||
// Load existing share info
|
// Load existing share info
|
||||||
loadCurrentShare();
|
loadCurrentShare();
|
||||||
|
|
@ -146,7 +173,6 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
|
||||||
void FtpSharePlugin::onShareToggled(bool checked)
|
void FtpSharePlugin::onShareToggled(bool checked)
|
||||||
{
|
{
|
||||||
m_nameEdit->setEnabled(checked);
|
m_nameEdit->setEnabled(checked);
|
||||||
m_anonymousCheckBox->setEnabled(checked);
|
|
||||||
m_usersWidget->setEnabled(checked);
|
m_usersWidget->setEnabled(checked);
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
}
|
}
|
||||||
|
|
@ -167,12 +193,10 @@ void FtpSharePlugin::loadCurrentShare()
|
||||||
// Format:
|
// Format:
|
||||||
// [sharename]
|
// [sharename]
|
||||||
// path=/some/path
|
// path=/some/path
|
||||||
// anonymous=y
|
|
||||||
// user_acl=user1:rw,user2:ro
|
// user_acl=user1:rw,user2:ro
|
||||||
QString currentSection;
|
QString currentSection;
|
||||||
QString sharePath;
|
QString sharePath;
|
||||||
QString shareAcl;
|
QString shareAcl;
|
||||||
QString anonymous;
|
|
||||||
|
|
||||||
const QStringList lines = output.split(QLatin1Char('\n'));
|
const QStringList lines = output.split(QLatin1Char('\n'));
|
||||||
for (const QString &line : lines) {
|
for (const QString &line : lines) {
|
||||||
|
|
@ -185,13 +209,10 @@ void FtpSharePlugin::loadCurrentShare()
|
||||||
currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1);
|
currentSection = line.mid(1, line.indexOf(QLatin1Char(']')) - 1);
|
||||||
sharePath.clear();
|
sharePath.clear();
|
||||||
shareAcl.clear();
|
shareAcl.clear();
|
||||||
anonymous.clear();
|
|
||||||
} else if (line.startsWith(QLatin1String("path="))) {
|
} else if (line.startsWith(QLatin1String("path="))) {
|
||||||
sharePath = line.mid(5);
|
sharePath = line.mid(5);
|
||||||
} else if (line.startsWith(QLatin1String("user_acl="))) {
|
} else if (line.startsWith(QLatin1String("user_acl="))) {
|
||||||
shareAcl = line.mid(9);
|
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_originalShareName = currentSection;
|
||||||
m_shareCheckBox->setChecked(true);
|
m_shareCheckBox->setChecked(true);
|
||||||
m_nameEdit->setText(currentSection);
|
m_nameEdit->setText(currentSection);
|
||||||
m_anonymousCheckBox->setChecked(anonymous == QLatin1String("y"));
|
|
||||||
|
|
||||||
// Parse ACL: "user1:rw,user2:ro"
|
// Parse ACL: "user1:rw,user2:ro"
|
||||||
if (!shareAcl.isEmpty()) {
|
if (!shareAcl.isEmpty()) {
|
||||||
|
|
@ -257,6 +277,41 @@ void FtpSharePlugin::applyChanges()
|
||||||
proc.waitForFinished(5000);
|
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
|
// Build ACL string
|
||||||
QStringList aclParts;
|
QStringList aclParts;
|
||||||
for (const auto &up : m_userPerms) {
|
for (const auto &up : m_userPerms) {
|
||||||
|
|
@ -267,14 +322,11 @@ void FtpSharePlugin::applyChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString aclString = aclParts.join(QLatin1Char(','));
|
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;
|
QProcess proc;
|
||||||
proc.start(QStringLiteral("dolphin-ftp-share"),
|
proc.start(QStringLiteral("dolphin-ftp-share"),
|
||||||
{QStringLiteral("add"), shareName, m_path, anonymous, aclString});
|
{QStringLiteral("add"), shareName, m_path, aclString});
|
||||||
proc.waitForFinished(5000);
|
proc.waitForFinished(5000);
|
||||||
|
|
||||||
if (proc.exitCode() == 0) {
|
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"
|
#include "ftpshareplugin.moc"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
struct UserPermission {
|
struct UserPermission {
|
||||||
|
|
@ -22,6 +23,7 @@ public:
|
||||||
private:
|
private:
|
||||||
void loadCurrentShare();
|
void loadCurrentShare();
|
||||||
void onShareToggled(bool checked);
|
void onShareToggled(bool checked);
|
||||||
|
void onChangePassword();
|
||||||
|
|
||||||
QString m_path;
|
QString m_path;
|
||||||
QString m_defaultShareName;
|
QString m_defaultShareName;
|
||||||
|
|
@ -29,8 +31,8 @@ private:
|
||||||
QWidget *m_page = nullptr;
|
QWidget *m_page = nullptr;
|
||||||
QCheckBox *m_shareCheckBox = nullptr;
|
QCheckBox *m_shareCheckBox = nullptr;
|
||||||
QLineEdit *m_nameEdit = nullptr;
|
QLineEdit *m_nameEdit = nullptr;
|
||||||
QCheckBox *m_anonymousCheckBox = nullptr;
|
|
||||||
QWidget *m_usersWidget = nullptr;
|
QWidget *m_usersWidget = nullptr;
|
||||||
|
QComboBox *m_pwUserCombo = nullptr;
|
||||||
|
|
||||||
QVector<UserPermission> m_userPerms;
|
QVector<UserPermission> m_userPerms;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,108 @@
|
||||||
#!/bin/bash
|
#!/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:
|
# Usage:
|
||||||
|
# dolphin-ftp-share setup Initial Pure-FTPd setup
|
||||||
# dolphin-ftp-share info List all shares
|
# 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
|
# dolphin-ftp-share delete <name> Remove a share
|
||||||
#
|
# dolphin-ftp-share passwd <username> <password> Set FTP password
|
||||||
# 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.
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SHARE_DIR="${HOME}/.local/share/dolphin-ftp-shares"
|
SHARE_DIR="${HOME}/.local/share/dolphin-ftp-shares"
|
||||||
FTP_ROOT="${HOME}/.local/share/dolphin-ftp-root"
|
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() {
|
cmd_info() {
|
||||||
[ -d "$SHARE_DIR" ] || exit 0
|
[ -d "$SHARE_DIR" ] || exit 0
|
||||||
|
|
@ -31,33 +118,49 @@ cmd_info() {
|
||||||
cmd_add() {
|
cmd_add() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local path="$2"
|
local path="$2"
|
||||||
local anon="$3"
|
local acl="$3"
|
||||||
local acl="$4"
|
|
||||||
|
|
||||||
if [ -z "$name" ] || [ -z "$path" ]; then
|
if [ -z "$name" ] || [ -z "$path" ]; then
|
||||||
echo "Error: name and path are required" >&2
|
echo "Error: name and path are required" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
cat > "$SHARE_DIR/$name.conf" <<EOF
|
||||||
path=$path
|
path=$path
|
||||||
anonymous=$anon
|
|
||||||
user_acl=$acl
|
user_acl=$acl
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Create mount point directory
|
# Process per-user ACL: "user1:ro,user2:rw"
|
||||||
mkdir -p "$FTP_ROOT/$name"
|
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
|
ensure_virtual_user "$username"
|
||||||
mountpoint -q "$FTP_ROOT/$name" 2>/dev/null && sudo umount "$FTP_ROOT/$name" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Bind-mount the shared folder into the FTP root
|
mkdir -p "$mount_point"
|
||||||
sudo mount --bind "$path" "$FTP_ROOT/$name"
|
|
||||||
|
|
||||||
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() {
|
cmd_delete() {
|
||||||
|
|
@ -68,32 +171,77 @@ cmd_delete() {
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Unmount bind mount
|
unmount_share "$name"
|
||||||
if mountpoint -q "$FTP_ROOT/$name" 2>/dev/null; then
|
|
||||||
sudo umount "$FTP_ROOT/$name"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$SHARE_DIR/$name.conf"
|
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 "$SHARE_DIR" 2>/dev/null || true
|
||||||
rmdir "$FTP_ROOT" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "Share '$name' deleted"
|
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
|
case "${1:-}" in
|
||||||
info) cmd_info ;;
|
setup) cmd_setup ;;
|
||||||
add) cmd_add "$2" "$3" "${4:-n}" "${5:-}" ;;
|
info) cmd_info ;;
|
||||||
delete) cmd_delete "$2" ;;
|
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 "" >&2
|
||||||
echo "Commands:" >&2
|
echo "Commands:" >&2
|
||||||
|
echo " setup Initial Pure-FTPd setup" >&2
|
||||||
echo " info List all shares" >&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 " 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
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue