Remove quota feature (incompatible with ZFS-backed LXC)
Linux setquota/repquota require quota support on the underlying filesystem. The target deployment is a Proxmox LXC on ZFS storage where this is not available inside the container. Removed: /api/quota endpoint, _quota_supported, _quota_usage, _find_quota_fs, quotas.js, quota modal, dashboard quota table, Quota button from share rows. PLANNING.md updated: notes quota as out of scope with reason. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
af315b37b2
commit
bf7daf1c2c
6 files changed
+4
-239
No files matched your search
@@ -415,58 +415,6 @@ def toggle_nfs():
|
||||
return jsonify({'status': 'enabled' if enabled else 'disabled'})
|
||||
|
||||
|
||||
# ===== QUOTA =====
|
||||
|
||||
def _find_quota_fs(path):
|
||||
"""Return the best filesystem argument for setquota.
|
||||
|
||||
setquota needs a path whose filesystem has quota enabled. We try the
|
||||
exact path first, then walk up to the mountpoint reported by df. If df
|
||||
says '/' we still try the path itself — the user may have quota on root.
|
||||
"""
|
||||
r = subprocess.run(['df', '--output=target', path], capture_output=True, text=True)
|
||||
lines = [l.strip() for l in r.stdout.strip().splitlines()
|
||||
if l.strip() and l.strip() != 'Mounted on']
|
||||
return lines[0] if lines else path
|
||||
|
||||
|
||||
@app.route('/api/quota', methods=['POST'])
|
||||
@require_auth
|
||||
def set_quota():
|
||||
data = request.json or {}
|
||||
user = data.get('user', '')
|
||||
raw_quota = data.get('quota', 0)
|
||||
path = data.get('path', '/srv')
|
||||
|
||||
if not VALID_NAME.match(user):
|
||||
return jsonify({'error': 'invalid username'}), 400
|
||||
try:
|
||||
quota_gb = int(raw_quota)
|
||||
if quota_gb <= 0:
|
||||
raise ValueError()
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'error': 'quota must be a positive integer (GB)'}), 400
|
||||
|
||||
try:
|
||||
safe_path = validate_path(path)
|
||||
except ValueError as e:
|
||||
return jsonify({'error': str(e)}), 400
|
||||
|
||||
mount = _find_quota_fs(safe_path)
|
||||
soft_kb = str(quota_gb * 1024 * 1024)
|
||||
hard_kb = str((quota_gb + 1) * 1024 * 1024)
|
||||
try:
|
||||
r = subprocess.run(
|
||||
['setquota', '-u', user, soft_kb, hard_kb, '0', '0', mount],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return jsonify({'error': r.stderr.strip() or 'setquota failed'}), 500
|
||||
except FileNotFoundError:
|
||||
return jsonify({'error': 'setquota not found — install quota package'}), 500
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
|
||||
# ===== USERS =====
|
||||
|
||||
def _smb_users():
|
||||
@@ -613,64 +561,15 @@ def _shares_with_usage():
|
||||
return result
|
||||
|
||||
|
||||
def _quota_supported():
|
||||
"""Return (supported: bool, reason: str). Checks filesystem type under /srv."""
|
||||
r = subprocess.run(['df', '-T', '/srv'], capture_output=True, text=True)
|
||||
for line in r.stdout.splitlines()[1:]:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
fstype = parts[1].lower()
|
||||
if fstype == 'zfs':
|
||||
return False, 'ZFS storage detected — Linux disk quotas are not available inside a ZFS-backed container. Manage per-dataset quotas from the Proxmox host with: zfs set quota=10G <dataset>'
|
||||
if fstype in ('tmpfs', 'overlay', 'aufs'):
|
||||
return False, f'{fstype} filesystem does not support disk quotas'
|
||||
return True, ''
|
||||
|
||||
|
||||
def _quota_usage():
|
||||
try:
|
||||
r = subprocess.run(['repquota', '-u', '/srv'], capture_output=True, text=True, timeout=10)
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
quotas = []
|
||||
past_sep = False
|
||||
for line in r.stdout.splitlines():
|
||||
if line.startswith('---'):
|
||||
past_sep = True
|
||||
continue
|
||||
if not past_sep:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 5 or not re.match(r'^[+-]{2}$', parts[1]):
|
||||
continue
|
||||
try:
|
||||
used_kb, soft_kb, hard_kb = int(parts[2]), int(parts[3]), int(parts[4])
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
if soft_kb == 0 and hard_kb == 0 and used_kb == 0:
|
||||
continue
|
||||
quotas.append({
|
||||
'user': parts[0],
|
||||
'used_gb': round(used_kb / 1024**2, 2),
|
||||
'soft_gb': round(soft_kb / 1024**2, 2),
|
||||
'hard_gb': round(hard_kb / 1024**2, 2),
|
||||
'percent': round(used_kb / soft_kb * 100, 1) if soft_kb else 0,
|
||||
})
|
||||
return quotas
|
||||
|
||||
|
||||
@app.route('/api/dashboard', methods=['GET'])
|
||||
@require_auth
|
||||
def get_dashboard():
|
||||
quota_ok, quota_reason = _quota_supported()
|
||||
return jsonify({
|
||||
'status': _daemon_status(),
|
||||
'disk': _disk_usage(),
|
||||
'shares': _shares_with_usage(),
|
||||
'quotas': _quota_usage() if quota_ok else [],
|
||||
'quota_supported': quota_ok,
|
||||
'quota_reason': quota_reason,
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user