- Backend: detect the actual filesystem mountpoint via 'df' instead of hardcoding /srv/samba — setquota needs the mountpoint, not a subpath - Backend: catch CalledProcessError and FileNotFoundError from setquota and return the actual error message (e.g. quota not enabled, package not installed) instead of crashing with 500 - Frontend: always send the share path in the quota API call so the backend can find the right filesystem, even for SMB per-user quotas - Frontend: read the actual error message from the response and show it in the toast instead of just "N quota(s) failed" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
77 lines
3.4 KiB
JavaScript
77 lines
3.4 KiB
JavaScript
let _qCtx = null;
|
|
|
|
function openQuotaModal(type, name, path, validUsers) {
|
|
_qCtx = { type, name, path, users: validUsers ? validUsers.split(/[\s,]+/).filter(Boolean) : [] };
|
|
document.getElementById('quotaModalTitle').textContent =
|
|
'Quota — ' + (type === 'smb' ? esc(name) : esc(path));
|
|
|
|
let body = '';
|
|
if (type === 'smb' && _qCtx.users.length > 0) {
|
|
body = _qCtx.users.map(u => `
|
|
<div class="mb-3">
|
|
<label class="form-label" style="color:var(--muted); font-size:.85rem">${esc(u)}</label>
|
|
<div class="input-group">
|
|
<input type="number" class="form-control quota-inp" data-user="${esc(u)}" placeholder="GB" min="1">
|
|
<span class="input-group-text">GB</span>
|
|
</div>
|
|
</div>`).join('');
|
|
} else {
|
|
const note = type === 'nfs'
|
|
? `<p class="mb-3" style="color:var(--muted); font-size:.87rem">Path: <code>${esc(path)}</code></p>`
|
|
: `<p class="mb-3" style="color:var(--muted); font-size:.87rem">No valid users on this share — enter a username manually.</p>`;
|
|
body = note + `
|
|
<div class="mb-3">
|
|
<label class="form-label" style="color:var(--muted); font-size:.85rem">Username</label>
|
|
<input type="text" id="qManualUser" class="form-control" placeholder="username">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" style="color:var(--muted); font-size:.85rem">Quota</label>
|
|
<div class="input-group">
|
|
<input type="number" id="qManualGB" class="form-control" placeholder="GB" min="1">
|
|
<span class="input-group-text">GB</span>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
document.getElementById('quotaModalBody').innerHTML = body;
|
|
new bootstrap.Modal(document.getElementById('quotaModal')).show();
|
|
}
|
|
|
|
async function submitQuota() {
|
|
if (!_qCtx) return;
|
|
const calls = [];
|
|
|
|
if (_qCtx.type === 'smb' && _qCtx.users.length > 0) {
|
|
for (const inp of document.querySelectorAll('.quota-inp')) {
|
|
const gb = parseInt(inp.value);
|
|
if (!gb || gb <= 0) continue;
|
|
calls.push({ user: inp.dataset.user, quota: gb, path: _qCtx.path });
|
|
}
|
|
} else {
|
|
const user = document.getElementById('qManualUser')?.value.trim();
|
|
const gb = parseInt(document.getElementById('qManualGB')?.value);
|
|
if (!user) return showToast('Username is required.', 'danger');
|
|
if (!gb || gb <= 0) return showToast('Quota must be a positive number.', 'danger');
|
|
calls.push({ user, path: _qCtx.path || '/srv', quota: gb });
|
|
}
|
|
|
|
if (!calls.length) return showToast('No quota values entered.', 'warning');
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('quotaModal')).hide();
|
|
|
|
const responses = await Promise.all(calls.map(body =>
|
|
apiFetch('/api/quota', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) })
|
|
));
|
|
const errors = [];
|
|
for (const r of responses) {
|
|
if (r && !r.ok) {
|
|
const d = await r.json().catch(() => ({}));
|
|
errors.push(d.error || `HTTP ${r.status}`);
|
|
}
|
|
}
|
|
if (errors.length) showToast('Quota error: ' + errors.join('; '), 'danger');
|
|
else showToast(`Quota${calls.length > 1 ? 's' : ''} applied.`);
|
|
|
|
loadDashboard();
|
|
}
|