diff --git a/install/pure-ftpd-one/pure-ftpd.conf b/install/pure-ftpd-one/pure-ftpd.conf
index 27f4d1544..9298bb32d 100644
--- a/install/pure-ftpd-one/pure-ftpd.conf
+++ b/install/pure-ftpd-one/pure-ftpd.conf
@@ -31,6 +31,6 @@ MaxDiskUsage 99
CustomerProof yes
TLS 1
PassivePortRange 40110 40210
-# Quota enforcement
-Quota yes
+# Quota enforcement (maxfiles:maxsizeMB; enables MySQL per-user quotas)
+Quota 100000:100000
diff --git a/install/pure-ftpd/pure-ftpd.conf b/install/pure-ftpd/pure-ftpd.conf
index 27f4d1544..9298bb32d 100644
--- a/install/pure-ftpd/pure-ftpd.conf
+++ b/install/pure-ftpd/pure-ftpd.conf
@@ -31,6 +31,6 @@ MaxDiskUsage 99
CustomerProof yes
TLS 1
PassivePortRange 40110 40210
-# Quota enforcement
-Quota yes
+# Quota enforcement (maxfiles:maxsizeMB; enables MySQL per-user quotas)
+Quota 100000:100000
diff --git a/serverStatus/static/serverStatus/serverStatus.js b/serverStatus/static/serverStatus/serverStatus.js
index f16d66701..733d26a96 100644
--- a/serverStatus/static/serverStatus/serverStatus.js
+++ b/serverStatus/static/serverStatus/serverStatus.js
@@ -641,11 +641,15 @@ app.controller('servicesManager', function ($scope, $http) {
getServiceStatus();
$scope.ActionSuccessfull = true;
$scope.ActionFailed = false;
+ $scope.actionErrorMsg = '';
$scope.couldNotConnect = false;
$scope.actionLoader = false;
$scope.btnDisable = false;
}, 3000);
} else {
+ var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Action failed';
+ if (errMsg === 0) errMsg = 'Action failed';
+ $scope.actionErrorMsg = errMsg;
setTimeout(function () {
getServiceStatus();
$scope.ActionSuccessfull = false;
@@ -654,7 +658,6 @@ app.controller('servicesManager', function ($scope, $http) {
$scope.actionLoader = false;
$scope.btnDisable = false;
}, 5000);
-
}
}
diff --git a/serverStatus/templates/serverStatus/services.html b/serverStatus/templates/serverStatus/services.html
index 11fdcd624..db6b6084d 100644
--- a/serverStatus/templates/serverStatus/services.html
+++ b/serverStatus/templates/serverStatus/services.html
@@ -622,6 +622,7 @@
{% trans "Action Failed" %}
+ {% trans "Details:" %}
diff --git a/serverStatus/views.py b/serverStatus/views.py
index 9d5d54bd2..8f051b86f 100644
--- a/serverStatus/views.py
+++ b/serverStatus/views.py
@@ -319,18 +319,36 @@ def servicesAction(request):
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
- else:
- if service == 'pure-ftpd':
- if os.path.exists("/etc/lsb-release"):
- service = 'pure-ftpd-mysql'
- else:
- service = 'pure-ftpd'
+ if service == 'pure-ftpd':
+ if os.path.exists("/etc/lsb-release"):
+ service = 'pure-ftpd-mysql'
+ else:
+ service = 'pure-ftpd'
- command = 'sudo systemctl %s %s' % (action, service)
- ProcessUtilities.executioner(command)
- final_dic = {'serviceAction': 1, "error_message": 0}
- final_json = json.dumps(final_dic)
- return HttpResponse(final_json)
+ # Run as root with shell so systemctl has permission (panel may run as lscpd)
+ command = 'systemctl %s %s' % (action, service)
+ ProcessUtilities.executioner(command, 'root', True)
+ time.sleep(1)
+
+ # For start action, verify service actually came up; return error if not
+ if action == 'start':
+ try:
+ out = ProcessUtilities.outputExecutioner('systemctl is-active %s' % service, 'root', True)
+ if not (out and out.strip() == 'active'):
+ status_out = ProcessUtilities.outputExecutioner(
+ 'systemctl status %s --no-pager -l 2>&1 | head -15' % service, 'root', True)
+ err_msg = (status_out or '').strip().replace('\n', ' ')[:400]
+ final_dic = {'serviceAction': 0, 'error_message': 'Service did not start. ' + err_msg}
+ final_json = json.dumps(final_dic)
+ return HttpResponse(final_json)
+ except Exception as e:
+ final_dic = {'serviceAction': 0, 'error_message': 'Service did not start: %s' % str(e)}
+ final_json = json.dumps(final_dic)
+ return HttpResponse(final_json)
+
+ final_dic = {'serviceAction': 1, "error_message": 0}
+ final_json = json.dumps(final_dic)
+ return HttpResponse(final_json)
except BaseException as msg:
diff --git a/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md b/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md
new file mode 100644
index 000000000..917206ec1
--- /dev/null
+++ b/to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md
@@ -0,0 +1,24 @@
+# Pure-FTPd Quota Syntax Fix (2026-02-04)
+
+## Problem
+Pure-FTPd failed to start with:
+```
+/etc/pure-ftpd/pure-ftpd.conf:35:1: syntax error line 35: [Quota ...].
+```
+
+## Cause
+The config used `Quota yes`, but Pure-FTPd expects **`Quota maxfiles:maxsize`** (e.g. `Quota 1000:10` for 1000 files and 10 MB). The value is not a boolean.
+
+## Fix applied
+
+### On the server
+- `/etc/pure-ftpd/pure-ftpd.conf`: line 35 set to `Quota 100000:100000` (high default so MySQL per-user quotas apply).
+- Service started successfully: `systemctl start pure-ftpd`.
+
+### In the repo
+- **install/pure-ftpd/pure-ftpd.conf** and **install/pure-ftpd-one/pure-ftpd.conf**: `Quota yes` → `Quota 100000:100000`.
+- **websiteFunctions/website.py** (`enableFTPQuota`): sed/echo now write `Quota 100000:100000` instead of `Quota yes` (or tabs).
+
+## Reference
+- Upstream: https://github.com/jedisct1/pure-ftpd/blob/master/pure-ftpd.conf.in (comment: "Quota 1000:10").
+- `pure-ftpd --help`: `-n --quota
`.
diff --git a/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html b/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html
index 8f2545a5c..b075aa0fd 100644
--- a/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html
+++ b/websiteFunctions/templates/websiteFunctions/ftpQuotaManagement.html
@@ -41,8 +41,8 @@ FTP Quota Management - CyberPanel
FTP Quota System
Enable and manage individual FTP user quotas. This allows you to set storage limits for each FTP user.
-
@@ -125,15 +125,79 @@ FTP Quota Management - CyberPanel