diff --git a/ftp/ftpManager.py b/ftp/ftpManager.py index d3372cee5..04cc0a003 100644 --- a/ftp/ftpManager.py +++ b/ftp/ftpManager.py @@ -336,6 +336,87 @@ class FTPManager: json_data = json.dumps(data_ret) return HttpResponse(json_data) + def getFTPQuotaUsage(self): + """ + Get quota usage information for an FTP user + """ + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if ACLManager.currentContextPermission(currentACL, 'listFTPAccounts') == 0: + return ACLManager.loadErrorJson('getQuotaUsage', 0) + + data = json.loads(self.request.body) + userName = data['ftpUserName'] + + admin = Administrator.objects.get(pk=userID) + ftp = Users.objects.get(user=userName) + + if currentACL['admin'] == 1: + pass + elif ftp.domain.admin != admin: + return ACLManager.loadErrorJson() + + result = FTPUtilities.getFTPQuotaUsage(userName) + + if isinstance(result, dict): + data_ret = { + 'status': 1, + 'getQuotaUsage': 1, + 'error_message': "None", + 'quota_usage': result + } + else: + data_ret = { + 'status': 0, + 'getQuotaUsage': 0, + 'error_message': result[1] if isinstance(result, tuple) else str(result) + } + + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'getQuotaUsage': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + def migrateFTPQuotas(self): + """ + Migrate existing FTP users to the new quota system + """ + try: + userID = self.request.session['userID'] + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] != 1: + return ACLManager.loadErrorJson('migrateQuotas', 0) + + result = FTPUtilities.migrateExistingFTPUsers() + + if result[0] == 1: + data_ret = { + 'status': 1, + 'migrateQuotas': 1, + 'error_message': "None", + 'message': result[1] + } + else: + data_ret = { + 'status': 0, + 'migrateQuotas': 0, + 'error_message': result[1] + } + + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + + except BaseException as msg: + data_ret = {'status': 0, 'migrateQuotas': 0, 'error_message': str(msg)} + json_data = json.dumps(data_ret) + return HttpResponse(json_data) + def installPureFTPD(self): def pureFTPDServiceName(): diff --git a/ftp/urls.py b/ftp/urls.py index b36e684c5..ce51a711e 100644 --- a/ftp/urls.py +++ b/ftp/urls.py @@ -16,4 +16,6 @@ urlpatterns = [ path('getAllFTPAccounts', views.getAllFTPAccounts, name='getAllFTPAccounts'), path('changePassword', views.changePassword, name='changePassword'), path('updateFTPQuota', views.updateFTPQuota, name='updateFTPQuota'), + path('getFTPQuotaUsage', views.getFTPQuotaUsage, name='getFTPQuotaUsage'), + path('migrateFTPQuotas', views.migrateFTPQuotas, name='migrateFTPQuotas'), ] diff --git a/ftp/views.py b/ftp/views.py index de6d5e98f..bc509386e 100644 --- a/ftp/views.py +++ b/ftp/views.py @@ -221,5 +221,19 @@ def updateFTPQuota(request): try: fm = FTPManager(request) return fm.updateFTPQuota() + except KeyError: + return redirect(loadLoginPage) + +def getFTPQuotaUsage(request): + try: + fm = FTPManager(request) + return fm.getFTPQuotaUsage() + except KeyError: + return redirect(loadLoginPage) + +def migrateFTPQuotas(request): + try: + fm = FTPManager(request) + return fm.migrateFTPQuotas() except KeyError: return redirect(loadLoginPage) \ No newline at end of file diff --git a/install/pure-ftpd-one/pure-ftpd.conf b/install/pure-ftpd-one/pure-ftpd.conf index 299252b61..27f4d1544 100644 --- a/install/pure-ftpd-one/pure-ftpd.conf +++ b/install/pure-ftpd-one/pure-ftpd.conf @@ -31,4 +31,6 @@ MaxDiskUsage 99 CustomerProof yes TLS 1 PassivePortRange 40110 40210 +# Quota enforcement +Quota yes diff --git a/install/pure-ftpd-one/pureftpd-mysql.conf b/install/pure-ftpd-one/pureftpd-mysql.conf index 9909a501c..f882c9600 100644 --- a/install/pure-ftpd-one/pureftpd-mysql.conf +++ b/install/pure-ftpd-one/pureftpd-mysql.conf @@ -7,5 +7,8 @@ MYSQLGetDir SELECT Dir FROM users WHERE User='\L' MYSQLGetGID SELECT Gid FROM users WHERE User='\L' MYSQLGetPW SELECT Password FROM users WHERE User='\L' MYSQLGetUID SELECT Uid FROM users WHERE User='\L' +# Quota enforcement queries +MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L' +MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L' MYSQLPassword 1qaz@9xvps MYSQLUser cyberpanel diff --git a/install/pure-ftpd/pure-ftpd.conf b/install/pure-ftpd/pure-ftpd.conf index 299252b61..27f4d1544 100644 --- a/install/pure-ftpd/pure-ftpd.conf +++ b/install/pure-ftpd/pure-ftpd.conf @@ -31,4 +31,6 @@ MaxDiskUsage 99 CustomerProof yes TLS 1 PassivePortRange 40110 40210 +# Quota enforcement +Quota yes diff --git a/install/pure-ftpd/pureftpd-mysql.conf b/install/pure-ftpd/pureftpd-mysql.conf index 2c3dff44e..f2e5548f0 100644 --- a/install/pure-ftpd/pureftpd-mysql.conf +++ b/install/pure-ftpd/pureftpd-mysql.conf @@ -7,5 +7,8 @@ MYSQLGetDir SELECT Dir FROM users WHERE User='\L' MYSQLGetGID SELECT Gid FROM users WHERE User='\L' MYSQLGetPW SELECT Password FROM users WHERE User='\L' MYSQLGetUID SELECT Uid FROM users WHERE User='\L' +# Quota enforcement queries +MYSQLGetQTAFS SELECT QuotaSize FROM users WHERE User='\L' +MYSQLGetQTAUS SELECT 0 FROM users WHERE User='\L' MYSQLPassword 1qaz@9xvps MYSQLUser cyberpanel diff --git a/plogical/ftpUtilities.py b/plogical/ftpUtilities.py index 0decb5952..551d98423 100644 --- a/plogical/ftpUtilities.py +++ b/plogical/ftpUtilities.py @@ -266,6 +266,9 @@ class FTPUtilities: ftp.save() + # Apply quota to filesystem if needed + FTPUtilities.applyQuotaToFilesystem(ftp) + return 1, "FTP quota updated successfully" except Users.DoesNotExist: @@ -274,6 +277,107 @@ class FTPUtilities: logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [updateFTPQuota]") return 0, str(msg) + @staticmethod + def applyQuotaToFilesystem(ftp_user): + """ + Apply quota settings to the filesystem level + """ + try: + import subprocess + + # Get the user's directory + user_dir = ftp_user.dir + if not user_dir or not os.path.exists(user_dir): + return False, "User directory not found" + + # Convert quota from MB to KB for setquota command + quota_kb = ftp_user.quotasize * 1024 + + # Apply quota using setquota command + # Note: This requires quota tools to be installed + try: + # Set both soft and hard limits to the same value + subprocess.run([ + 'setquota', '-u', str(ftp_user.uid), + f'{quota_kb}K', f'{quota_kb}K', + '0', '0', # inode limits (unlimited) + user_dir + ], check=True, capture_output=True) + + logging.CyberCPLogFileWriter.writeToFile(f"Applied quota {quota_kb}KB to user {ftp_user.user} in {user_dir}") + return True, "Quota applied successfully" + + except subprocess.CalledProcessError as e: + logging.CyberCPLogFileWriter.writeToFile(f"Failed to apply quota: {e}") + return False, f"Failed to apply quota: {e}" + except FileNotFoundError: + # setquota command not found, quota tools not installed + logging.CyberCPLogFileWriter.writeToFile("setquota command not found - quota tools may not be installed") + return False, "Quota tools not installed" + + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f"Error applying quota to filesystem: {str(e)}") + return False, str(e) + + @staticmethod + def getFTPQuotaUsage(ftpUsername): + """ + Get current quota usage for an FTP user + """ + try: + ftp = Users.objects.get(user=ftpUsername) + user_dir = ftp.dir + + if not user_dir or not os.path.exists(user_dir): + return 0, "User directory not found" + + # Get directory size in MB + import subprocess + result = subprocess.run(['du', '-sm', user_dir], capture_output=True, text=True) + + if result.returncode == 0: + usage_mb = int(result.stdout.split()[0]) + quota_mb = ftp.quotasize + usage_percent = (usage_mb / quota_mb * 100) if quota_mb > 0 else 0 + + return { + 'usage_mb': usage_mb, + 'quota_mb': quota_mb, + 'usage_percent': round(usage_percent, 2), + 'remaining_mb': max(0, quota_mb - usage_mb) + } + else: + return 0, "Failed to get directory size" + + except Users.DoesNotExist: + return 0, "FTP user not found" + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f"Error getting quota usage: {str(e)}") + return 0, str(e) + + @staticmethod + def migrateExistingFTPUsers(): + """ + Migrate existing FTP users to use the new quota system + """ + try: + migrated_count = 0 + + for ftp_user in Users.objects.all(): + # If custom_quota_enabled is not set, set it to False and use package default + if not hasattr(ftp_user, 'custom_quota_enabled') or ftp_user.custom_quota_enabled is None: + ftp_user.custom_quota_enabled = False + ftp_user.custom_quota_size = 0 + ftp_user.quotasize = ftp_user.domain.package.diskSpace + ftp_user.save() + migrated_count += 1 + + return 1, f"Migrated {migrated_count} FTP users to new quota system" + + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f"Error migrating FTP users: {str(e)}") + return 0, str(e) + def main(): diff --git a/scripts/enable_ftp_quota.sh b/scripts/enable_ftp_quota.sh new file mode 100644 index 000000000..6d6d7f5c6 --- /dev/null +++ b/scripts/enable_ftp_quota.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +# Enable FTP User Quota Feature +# This script applies the quota configuration and restarts Pure-FTPd + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Logging function +log_message() { + echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a /var/log/cyberpanel_ftp_quota.log +} + +log_message "${BLUE}Starting FTP Quota Feature Setup...${NC}" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + log_message "${RED}Please run as root${NC}" + exit 1 +fi + +# Backup existing configurations +log_message "${YELLOW}Backing up existing Pure-FTPd configurations...${NC}" + +if [ -f /etc/pure-ftpd/pure-ftpd.conf ]; then + cp /etc/pure-ftpd/pure-ftpd.conf /etc/pure-ftpd/pure-ftpd.conf.backup.$(date +%Y%m%d_%H%M%S) + log_message "${GREEN}Backed up pure-ftpd.conf${NC}" +fi + +if [ -f /etc/pure-ftpd/pureftpd-mysql.conf ]; then + cp /etc/pure-ftpd/pureftpd-mysql.conf /etc/pure-ftpd/pureftpd-mysql.conf.backup.$(date +%Y%m%d_%H%M%S) + log_message "${GREEN}Backed up pureftpd-mysql.conf${NC}" +fi + +# Apply new configurations +log_message "${YELLOW}Applying FTP quota configurations...${NC}" + +# Copy the updated configurations +if [ -f /usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf ]; then + cp /usr/local/CyberCP/install/pure-ftpd/pure-ftpd.conf /etc/pure-ftpd/pure-ftpd.conf + log_message "${GREEN}Updated pure-ftpd.conf${NC}" +fi + +if [ -f /usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf ]; then + cp /usr/local/CyberCP/install/pure-ftpd/pureftpd-mysql.conf /etc/pure-ftpd/pureftpd-mysql.conf + log_message "${GREEN}Updated pureftpd-mysql.conf${NC}" +fi + +# Check if Pure-FTPd is running +if systemctl is-active --quiet pure-ftpd; then + log_message "${YELLOW}Restarting Pure-FTPd service...${NC}" + systemctl restart pure-ftpd + + if systemctl is-active --quiet pure-ftpd; then + log_message "${GREEN}Pure-FTPd restarted successfully${NC}" + else + log_message "${RED}Failed to restart Pure-FTPd${NC}" + exit 1 + fi +else + log_message "${YELLOW}Starting Pure-FTPd service...${NC}" + systemctl start pure-ftpd + + if systemctl is-active --quiet pure-ftpd; then + log_message "${GREEN}Pure-FTPd started successfully${NC}" + else + log_message "${RED}Failed to start Pure-FTPd${NC}" + exit 1 + fi +fi + +# Verify quota enforcement is working +log_message "${YELLOW}Verifying quota enforcement...${NC}" + +# Check if quota queries are in the configuration +if grep -q "MYSQLGetQTAFS" /etc/pure-ftpd/pureftpd-mysql.conf; then + log_message "${GREEN}Quota queries found in MySQL configuration${NC}" +else + log_message "${RED}Quota queries not found in MySQL configuration${NC}" + exit 1 +fi + +if grep -q "Quota.*yes" /etc/pure-ftpd/pure-ftpd.conf; then + log_message "${GREEN}Quota enforcement enabled in Pure-FTPd configuration${NC}" +else + log_message "${RED}Quota enforcement not enabled in Pure-FTPd configuration${NC}" + exit 1 +fi + +# Test database connection +log_message "${YELLOW}Testing database connection...${NC}" + +# Get database credentials from configuration +MYSQL_USER=$(grep "MYSQLUser" /etc/pure-ftpd/pureftpd-mysql.conf | cut -d' ' -f2) +MYSQL_PASS=$(grep "MYSQLPassword" /etc/pure-ftpd/pureftpd-mysql.conf | cut -d' ' -f2) +MYSQL_DB=$(grep "MYSQLDatabase" /etc/pure-ftpd/pureftpd-mysql.conf | cut -d' ' -f2) + +if mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -e "USE $MYSQL_DB; SELECT COUNT(*) FROM users;" >/dev/null 2>&1; then + log_message "${GREEN}Database connection successful${NC}" +else + log_message "${RED}Database connection failed${NC}" + exit 1 +fi + +log_message "${GREEN}FTP User Quota feature has been successfully enabled!${NC}" +log_message "${BLUE}Features enabled:${NC}" +log_message " - Individual FTP user quotas" +log_message " - Custom quota sizes per user" +log_message " - Package default quota fallback" +log_message " - Real-time quota enforcement by Pure-FTPd" +log_message " - Web interface for quota management" + +log_message "${YELLOW}Note: Existing FTP users will need to have their quotas updated through the web interface to take effect.${NC}" + +exit 0