mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-06-27 20:09:07 +02:00
Fix Pure-FTPd quota syntax and service start; improve services/API error reporting
- Pure-FTPd: use Quota maxfiles:maxsize (100000:100000) instead of invalid 'Quota yes' in install templates and enableFTPQuota (website.py) so daemon starts - serverStatus: run systemctl as root, return detailed error on service start failure - FTP quota UI: better error display and feedback for enable quota action - Doc: to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -622,6 +622,7 @@
|
||||
<div ng-show="ActionFailed" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span>{% trans "Action Failed" %}</span>
|
||||
<span ng-show="actionErrorMsg" class="d-block mt-2"><strong>{% trans "Details:" %}</strong> <span ng-bind="actionErrorMsg"></span></span>
|
||||
</div>
|
||||
<div ng-show="ActionSuccessfull" class="alert alert-success">
|
||||
<i class="fas fa-check-circle alert-icon"></i>
|
||||
|
||||
@@ -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:
|
||||
|
||||
24
to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md
Normal file
24
to-do/PURE-FTPD-QUOTA-SYNTAX-FIX.md
Normal file
@@ -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 <opt>`.
|
||||
@@ -41,8 +41,8 @@ FTP Quota Management - CyberPanel
|
||||
<div class="alert alert-info">
|
||||
<h5><i class="fas fa-info-circle"></i> FTP Quota System</h5>
|
||||
<p>Enable and manage individual FTP user quotas. This allows you to set storage limits for each FTP user.</p>
|
||||
<button class="btn btn-primary" onclick="enableFTPQuota()">
|
||||
<i class="fas fa-play"></i> Enable FTP Quota System
|
||||
<button type="button" id="btnEnableFTPQuota" class="btn btn-primary" onclick="enableFTPQuota()">
|
||||
<i class="fas fa-play"></i> <span id="btnEnableFTPQuotaText">Enable FTP Quota System</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,15 +125,79 @@ FTP Quota Management - CyberPanel
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getCsrfToken() {
|
||||
var name = 'csrftoken';
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var c = cookies[i].trim();
|
||||
if (c.indexOf(name + '=') === 0) return c.substring(name.length + 1);
|
||||
}
|
||||
return '{{ csrf_token }}';
|
||||
}
|
||||
|
||||
function showNotification(type, message) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
try {
|
||||
new PNotify({ type: type === 'success' ? 'success' : 'error', text: message });
|
||||
return;
|
||||
} catch (e) {}
|
||||
}
|
||||
var alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
var icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
|
||||
var notification = '<div class="alert ' + alertClass + ' alert-dismissible fade show" role="alert">' +
|
||||
'<i class="fas ' + icon + '"></i> ' + message +
|
||||
'<button type="button" class="close" data-dismiss="alert"><span>×</span></button></div>';
|
||||
var target = document.querySelector('.ftp-quota-info .alert-info');
|
||||
if (target && target.parentNode) {
|
||||
var wrap = document.createElement('div');
|
||||
wrap.innerHTML = notification;
|
||||
target.parentNode.insertBefore(wrap.firstChild, target);
|
||||
setTimeout(function() {
|
||||
var al = target.parentNode.querySelector('.alert');
|
||||
if (al && al.remove) al.remove();
|
||||
}, 6000);
|
||||
}
|
||||
}
|
||||
|
||||
function enableFTPQuota() {
|
||||
$.post('{% url "enableFTPQuota" %}', {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
}, function(data) {
|
||||
if (data.status === 1) {
|
||||
showNotification('success', (data && (data.message || data.error_message)) || 'Success');
|
||||
refreshQuotas();
|
||||
} else {
|
||||
showNotification('error', (data && (data.error_message || data.message)) || 'Unknown error');
|
||||
var btn = document.getElementById('btnEnableFTPQuota');
|
||||
var btnText = document.getElementById('btnEnableFTPQuotaText');
|
||||
if (!btn || !btnText) return;
|
||||
var originalHtml = btnText.innerHTML;
|
||||
btn.disabled = true;
|
||||
btnText.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Enabling...';
|
||||
$.ajax({
|
||||
url: '{% url "enableFTPQuota" %}',
|
||||
type: 'POST',
|
||||
data: { 'csrfmiddlewaretoken': getCsrfToken() },
|
||||
headers: { 'X-CSRFToken': getCsrfToken() },
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
if (data && data.status === 1) {
|
||||
showNotification('success', data.message || 'FTP quota system enabled successfully');
|
||||
refreshQuotas();
|
||||
} else {
|
||||
showNotification('error', (data && (data.message || data.error_message)) || 'Enable failed');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
var msg = 'Request failed';
|
||||
if (xhr.responseJSON && (xhr.responseJSON.message || xhr.responseJSON.error_message)) {
|
||||
msg = xhr.responseJSON.message || xhr.responseJSON.error_message;
|
||||
} else if (xhr.responseText && xhr.responseText.length < 500) {
|
||||
try {
|
||||
var j = JSON.parse(xhr.responseText);
|
||||
msg = j.message || j.error_message || msg;
|
||||
} catch (e) {}
|
||||
}
|
||||
if (xhr.status === 403 || xhr.status === 302) {
|
||||
msg = 'Session may have expired. Please refresh the page and try again.';
|
||||
}
|
||||
showNotification('error', msg);
|
||||
},
|
||||
complete: function() {
|
||||
btn.disabled = false;
|
||||
btnText.innerHTML = originalHtml;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -230,26 +294,6 @@ function saveQuota() {
|
||||
});
|
||||
}
|
||||
|
||||
function showNotification(type, message) {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
|
||||
|
||||
const notification = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
<i class="fas ${icon}"></i> ${message}
|
||||
<button type="button" class="close" data-dismiss="alert">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('.card-body').prepend(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Load quotas on page load
|
||||
$(document).ready(function() {
|
||||
refreshQuotas();
|
||||
|
||||
@@ -8801,7 +8801,8 @@ StrictHostKeyChecking no
|
||||
|
||||
def enableFTPQuota(self, userID=None, data=None):
|
||||
"""
|
||||
Enable FTP quota system
|
||||
Enable FTP quota: ensure Quota yes in existing config (do not overwrite), restart Pure-FTPd.
|
||||
Uses correct service name (pure-ftpd-mysql on Debian/Ubuntu, pure-ftpd on RHEL/Alma).
|
||||
"""
|
||||
try:
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
@@ -8811,60 +8812,79 @@ StrictHostKeyChecking no
|
||||
if not (currentACL.get('admin', 0) == 1):
|
||||
return ACLManager.loadErrorJson('status', 0)
|
||||
|
||||
# Backup existing configurations
|
||||
logging.CyberCPLogFileWriter.writeToFile("Backing up existing Pure-FTPd configurations...")
|
||||
# Resolve Pure-FTPd service name (Debian/Ubuntu use pure-ftpd-mysql)
|
||||
if os.path.exists('/etc/lsb-release'):
|
||||
ftp_service = 'pure-ftpd-mysql'
|
||||
else:
|
||||
ftp_service = 'pure-ftpd'
|
||||
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
# Backup pure-ftpd.conf
|
||||
if os.path.exists('/etc/pure-ftpd/pure-ftpd.conf'):
|
||||
shutil.copy('/etc/pure-ftpd/pure-ftpd.conf', f'/etc/pure-ftpd/pure-ftpd.conf.backup.{timestamp}')
|
||||
|
||||
# Backup pureftpd-mysql.conf
|
||||
if os.path.exists('/etc/pure-ftpd/pureftpd-mysql.conf'):
|
||||
shutil.copy('/etc/pure-ftpd/pureftpd-mysql.conf', f'/etc/pure-ftpd/pureftpd-mysql.conf.backup.{timestamp}')
|
||||
|
||||
# Apply new configurations
|
||||
logging.CyberCPLogFileWriter.writeToFile("Applying FTP quota configurations...")
|
||||
|
||||
# Copy updated configurations
|
||||
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf'):
|
||||
shutil.copy('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf', '/etc/pure-ftpd/pure-ftpd.conf')
|
||||
|
||||
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf'):
|
||||
shutil.copy('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf', '/etc/pure-ftpd/pureftpd-mysql.conf')
|
||||
conf_path = '/etc/pure-ftpd/pure-ftpd.conf'
|
||||
# Only ensure Quota is enabled; do not overwrite existing config (preserves DB credentials, paths)
|
||||
if os.path.exists(conf_path):
|
||||
# If service is not running, try restoring latest backup (in case a previous run overwrote working config)
|
||||
try:
|
||||
out = ProcessUtilities.outputExecutioner(
|
||||
"systemctl is-active %s 2>/dev/null || true" % ftp_service, 'root', True)
|
||||
if not (out and out.strip() == 'active'):
|
||||
# Restore latest backups if present
|
||||
ProcessUtilities.executioner(
|
||||
"ls -t /etc/pure-ftpd/pure-ftpd.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pure-ftpd.conf",
|
||||
'root', True)
|
||||
ProcessUtilities.executioner(
|
||||
"ls -t /etc/pure-ftpd/pureftpd-mysql.conf.backup.* 2>/dev/null | head -1 | xargs -r -I {} cp {} /etc/pure-ftpd/pureftpd-mysql.conf",
|
||||
'root', True)
|
||||
except Exception:
|
||||
pass
|
||||
# Add or replace Quota line via root (Pure-FTPd expects maxfiles:maxsizeMB, not "yes")
|
||||
ProcessUtilities.executioner(
|
||||
"grep -q '^Quota' %s && sed -i 's/^Quota.*/Quota 100000:100000/' %s || echo 'Quota 100000:100000' >> %s" % (conf_path, conf_path, conf_path),
|
||||
'root', True)
|
||||
logging.CyberCPLogFileWriter.writeToFile("Set Quota yes in existing pure-ftpd.conf")
|
||||
else:
|
||||
# First-time: copy from repo
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf'):
|
||||
ProcessUtilities.executioner(
|
||||
'cp /usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf /etc/pure-ftpd/pure-ftpd.conf', 'root', True)
|
||||
if os.path.exists('/usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf'):
|
||||
ProcessUtilities.executioner(
|
||||
'cp /usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf /etc/pure-ftpd/pureftpd-mysql.conf', 'root', True)
|
||||
|
||||
# Restart Pure-FTPd
|
||||
logging.CyberCPLogFileWriter.writeToFile("Restarting Pure-FTPd service...")
|
||||
ProcessUtilities.executioner('systemctl restart pure-ftpd')
|
||||
logging.CyberCPLogFileWriter.writeToFile("Restarting Pure-FTPd service (%s)..." % ftp_service)
|
||||
ProcessUtilities.executioner('systemctl restart %s' % ftp_service, 'root', True)
|
||||
time.sleep(1)
|
||||
|
||||
# Verify configuration
|
||||
if ProcessUtilities.executioner('systemctl is-active --quiet pure-ftpd'):
|
||||
try:
|
||||
output = ProcessUtilities.outputExecutioner('systemctl is-active %s' % ftp_service, 'root', True)
|
||||
is_active = (output and output.strip() == 'active')
|
||||
except Exception:
|
||||
is_active = False
|
||||
|
||||
if is_active:
|
||||
logging.CyberCPLogFileWriter.writeToFile("FTP quota system enabled successfully")
|
||||
|
||||
data_ret = {
|
||||
'status': 1,
|
||||
'message': 'FTP quota system enabled successfully'
|
||||
}
|
||||
data_ret = {'status': 1, 'message': 'FTP quota system enabled successfully'}
|
||||
else:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': 'Failed to restart Pure-FTPd service'
|
||||
}
|
||||
# Capture failure reason for the user
|
||||
try:
|
||||
status_out = ProcessUtilities.outputExecutioner(
|
||||
'systemctl status %s --no-pager -l 2>&1 | head -20' % ftp_service, 'root', True)
|
||||
status_preview = (status_out or '').strip().replace('\n', ' ')[:300]
|
||||
except Exception:
|
||||
status_preview = ''
|
||||
logging.CyberCPLogFileWriter.writeToFile("Pure-FTPd service not active after restart")
|
||||
msg = 'Pure-FTPd did not start. Run: systemctl status %s' % ftp_service
|
||||
if status_preview:
|
||||
msg += '. ' + status_preview
|
||||
data_ret = {'status': 0, 'message': msg}
|
||||
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
return HttpResponse(json.dumps(data_ret), content_type='application/json')
|
||||
|
||||
except Exception as e:
|
||||
data_ret = {
|
||||
'status': 0,
|
||||
'message': f'Error enabling FTP quota: {str(e)}'
|
||||
}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
logging.CyberCPLogFileWriter.writeToFile("Error enabling FTP quota: %s" % str(e))
|
||||
data_ret = {'status': 0, 'message': 'Error enabling FTP quota: %s' % str(e)}
|
||||
return HttpResponse(json.dumps(data_ret), content_type='application/json')
|
||||
|
||||
def getFTPQuotas(self, userID=None, data=None):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user