Initial commit
This commit is contained in:
commit
6e3d1c0d67
|
|
@ -0,0 +1,187 @@
|
|||
# GlobalQuota - Nextcloud App v1.2.1
|
||||
|
||||
Define and monitor a global storage quota, compatible with S3 and external storage.
|
||||
|
||||
**NEW v1.2.1**: Enhanced UI reliability with fallback text display and improved debugging.
|
||||
**NEW v1.2.0**: Adaptive visualization with ServerInfo integration or standalone Chart.js donut.
|
||||
**NEW v1.1.0**: Blocks uploads when global quota is exceeded.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
* ✅ **Global quota definition and monitoring**
|
||||
* ✅ **Compatible with S3 and external storage backends**
|
||||
* ✅ **Upload blocking when quota exceeded** (PSR-14 events)
|
||||
* ✅ **OCC commands** for management
|
||||
* ✅ **REST API** for external integration
|
||||
* ✅ **Server Info dashboard integration**
|
||||
* ✅ **Adaptive visualization (v1.2.0)**:
|
||||
+ Integrates seamlessly into ServerInfo if supported
|
||||
+ Shows its own donut chart in Admin Settings otherwise
|
||||
* ✅ **Enhanced reliability (v1.2.1)**:
|
||||
+ Always displays quota values in text format
|
||||
+ Robust error handling and debugging logs
|
||||
+ Chart.js fallback mechanisms
|
||||
|
||||
---
|
||||
|
||||
### Admin Settings Panel
|
||||
The app displays quota information with both visual chart and text values:
|
||||
- **Used storage**: Shows current usage
|
||||
- **Free storage**: Available space remaining
|
||||
- **Total storage**: Global quota limit
|
||||
- **Usage percentage**: Visual indicator with color coding
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Download** or clone this repository
|
||||
2. **Copy** the `globalquota` folder to your Nextcloud `apps/` directory
|
||||
3. **Enable** the app in Nextcloud Admin Settings → Apps
|
||||
4. **Configure** the global quota in Admin Settings → Additional Settings → Global Quota
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to `config/config.php`:
|
||||
|
||||
```php
|
||||
'globalquota' => [
|
||||
'enabled' => true,
|
||||
'quota_bytes' => 500 * 1024 * 1024 * 1024, // 500 GB
|
||||
],
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OCC Commands
|
||||
|
||||
```bash
|
||||
# Set quota (in bytes)
|
||||
occ globalquota:set 2147483648 # 2 GB
|
||||
|
||||
# Check status
|
||||
occ globalquota:status
|
||||
|
||||
# Force recalculation
|
||||
occ globalquota:recalc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
```bash
|
||||
# Get current status (new endpoint for admin chart)
|
||||
curl -H "Authorization: Bearer TOKEN" \
|
||||
https://instance.com/apps/globalquota/status
|
||||
|
||||
# Legacy API v1 endpoint
|
||||
curl -H "Authorization: Bearer TOKEN" \
|
||||
https://instance.com/apps/globalquota/api/v1/status
|
||||
|
||||
# Update quota
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"quota_bytes": 10737418240}' \
|
||||
https://instance.com/apps/globalquota/api/v1/quota
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Upload Blocking
|
||||
|
||||
When the global quota is exceeded, the app will:
|
||||
|
||||
* ❌ **Block new file uploads**
|
||||
* ❌ **Block file updates that increase size**
|
||||
* 🚫 **Return HTTP 507 "Insufficient Storage" error**
|
||||
* 📱 **Work with all clients** (web, desktop, mobile, WebDAV)
|
||||
|
||||
---
|
||||
|
||||
## Visualization Modes
|
||||
|
||||
### 🔗 **With ServerInfo Integration**
|
||||
If ServerInfo supports events (`LoadAdditionalDataEvent`), GlobalQuota **replaces the Disk chart** with GlobalQuota data.
|
||||
|
||||
### 📊 **Standalone Mode**
|
||||
If ServerInfo does not support events, GlobalQuota shows its **own donut chart** in the Admin Settings section.
|
||||
|
||||
### 📝 **Text Fallback (v1.2.1)**
|
||||
Even if the chart fails to load, quota values are **always displayed in text format** for reliability.
|
||||
|
||||
**Both modes display:**
|
||||
- Usage percentage with color coding
|
||||
- Used / Free / Total values (formatted)
|
||||
- Auto-refresh capability via "Refresh" button
|
||||
- Detailed console logging for debugging
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Chart not displaying?
|
||||
1. Check browser console for errors (`F12` → Console)
|
||||
2. Look for `GlobalQuota:` log messages
|
||||
3. Verify Chart.js is loading (CDN or local)
|
||||
4. Text values should still display regardless
|
||||
|
||||
### API errors?
|
||||
1. Verify app is enabled and configured
|
||||
2. Check Nextcloud logs for backend errors
|
||||
3. Test endpoints directly with curl
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
* **Nextcloud 25+**
|
||||
* **Admin privileges** for configuration
|
||||
* **Optional**: ServerInfo app (for dashboard integration)
|
||||
* **Browser**: Modern browser with JavaScript enabled
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
* **v1.2.1** *(Latest)*
|
||||
+ Enhanced UI reliability with always-visible text values
|
||||
+ Improved error handling and debugging logs
|
||||
+ Chart.js fallback mechanisms
|
||||
+ Better console logging for troubleshooting
|
||||
|
||||
* **v1.2.0**
|
||||
+ Adaptive visualization (ServerInfo integration or standalone Chart.js donut)
|
||||
+ New `/apps/globalquota/status` endpoint
|
||||
+ Improved admin settings panel
|
||||
|
||||
* **v1.1.0**
|
||||
+ Upload blocking functionality with PSR-14 events
|
||||
+ OCC commands and REST API integration
|
||||
|
||||
* **v1.0.0**
|
||||
+ Initial release with basic quota management and S3 compatibility
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
1. **Fork** this repository
|
||||
2. **Create** a feature branch
|
||||
3. **Make** your changes
|
||||
4. **Test** thoroughly
|
||||
5. **Submit** a pull request
|
||||
|
||||
### Reporting Issues
|
||||
Please report bugs and feature requests on [GitHub Issues](https://github.com/DavidCamejo/globalquota/issues).
|
||||
|
||||
---
|
||||
|
||||
**Author:** David Camejo
|
||||
**License:** AGPL-3.0
|
||||
**Repository:** https://github.com/DavidCamejo/globalquota
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
/**
|
||||
* Load application class
|
||||
*/
|
||||
namespace OCA\GlobalQuota\AppInfo;
|
||||
|
||||
$app = new Application();
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0"?>
|
||||
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
|
||||
<id>globalquota</id>
|
||||
<name>Global Quota</name>
|
||||
<summary>Manage global storage quota with adaptive visualization</summary>
|
||||
<description>Global Quota allows administrators to set and monitor a global storage limit across all users. Integrates with ServerInfo if available, or shows its own chart.</description>
|
||||
<version>1.2.0</version>
|
||||
<licence>agpl</licence>
|
||||
<author>David Camejo</author>
|
||||
<namespace>GlobalQuota</namespace>
|
||||
<category>tools</category>
|
||||
<dependencies>
|
||||
<nextcloud min-version="25" max-version="30"/>
|
||||
</dependencies>
|
||||
<commands>
|
||||
<command>OCA\GlobalQuota\Command\StatusCommand</command>
|
||||
<command>OCA\GlobalQuota\Command\SetQuotaCommand</command>
|
||||
<command>OCA\GlobalQuota\Command\RecalcCommand</command>
|
||||
</commands>
|
||||
<settings>
|
||||
<admin>OCA\GlobalQuota\Settings\Admin\Settings</admin>
|
||||
<admin-section>OCA\GlobalQuota\Settings\Admin\Section</admin-section>
|
||||
</settings>
|
||||
</info>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
['name' => 'quota#status', 'url' => '/status', 'verb' => 'GET'],
|
||||
['name' => 'quota#apiStatus', 'url' => '/api/v1/status', 'verb' => 'GET'],
|
||||
['name' => 'quota#updateQuota', 'url' => '/api/v1/quota', 'verb' => 'PUT'],
|
||||
['name' => 'quota#setQuota', 'url' => '/set', 'verb' => 'POST'],
|
||||
['name' => 'quota#getQuota', 'url' => '/quota', 'verb' => 'GET'],
|
||||
['name' => 'quota#recalc', 'url' => '/recalc', 'verb' => 'GET'],
|
||||
]
|
||||
];
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
#globalquota.section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.globalquota-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.globalquota-chart-wrapper {
|
||||
width: 280px;
|
||||
min-height: 260px;
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-main-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.globalquota-stats {
|
||||
flex: 1;
|
||||
min-width: 260px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-main-background);
|
||||
}
|
||||
|
||||
.globalquota-stats h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.globalquota-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.globalquota-list li {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.globalquota-error {
|
||||
color: #b71c1c;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* GlobalQuota Admin Settings Styles
|
||||
*/
|
||||
|
||||
#globalquota-settings {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
#globalquota-settings h2 {
|
||||
margin-bottom: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.globalquota-chart-container {
|
||||
background: var(--color-main-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.globalquota-chart-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#globalquota-chart {
|
||||
max-width: 280px;
|
||||
max-height: 280px;
|
||||
}
|
||||
|
||||
.globalquota-stats {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--color-border-dark);
|
||||
padding: 6px 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.stat-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.globalquota-actions {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.globalquota-actions .button {
|
||||
padding: 6px 14px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<circle cx="32" cy="32" r="30" stroke="black" stroke-width="4" fill="#0078d7"/>
|
||||
<text x="32" y="37" font-size="20" text-anchor="middle" fill="white">GQ</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 231 B |
|
|
@ -0,0 +1,359 @@
|
|||
/**
|
||||
* GlobalQuota Admin Settings JavaScript
|
||||
* Maneja el gráfico donut y las acciones de administración
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Validate Nextcloud environment
|
||||
if (typeof OC === 'undefined' || typeof t === 'undefined') {
|
||||
console.error('GlobalQuota: Nextcloud environment not available');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('GlobalQuota admin JS loaded ✅');
|
||||
|
||||
let quotaChart = null;
|
||||
let chartInitialized = false;
|
||||
let dataLoaded = false;
|
||||
|
||||
function initChart() {
|
||||
const canvas = document.getElementById('globalquota-chart');
|
||||
if (!canvas) {
|
||||
console.warn('GlobalQuota: canvas #globalquota-chart no encontrado');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof Chart === 'undefined') {
|
||||
console.warn('GlobalQuota: Chart.js no está disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
quotaChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: [t('globalquota', 'Used'), t('globalquota', 'Free')],
|
||||
datasets: [{
|
||||
data: [0, 100],
|
||||
backgroundColor: ['#e74c3c', '#ecf0f1'],
|
||||
borderWidth: 0,
|
||||
cutout: '70%'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
padding: 20,
|
||||
usePointStyle: true,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = formatBytes(context.raw);
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = total > 0 ? ((context.raw / total) * 100).toFixed(1) : '0.0';
|
||||
return `${label}: ${value} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chartInitialized = true;
|
||||
console.log('GlobalQuota: Gráfico inicializado correctamente');
|
||||
|
||||
} catch (error) {
|
||||
console.error('GlobalQuota: Error al inicializar el gráfico:', error);
|
||||
quotaChart = null;
|
||||
chartInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadQuotaData() {
|
||||
// Prevent multiple simultaneous requests
|
||||
if (dataLoaded) {
|
||||
console.log('GlobalQuota: Data already loading, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
dataLoaded = true;
|
||||
console.log('GlobalQuota: Cargando datos...');
|
||||
|
||||
// Clear any previous errors
|
||||
const errorEl = document.getElementById('quota-error');
|
||||
if (errorEl) {
|
||||
errorEl.style.display = 'none';
|
||||
errorEl.textContent = '';
|
||||
}
|
||||
|
||||
// Try the new /status endpoint first
|
||||
fetch(OC.generateUrl('/apps/globalquota/status'))
|
||||
.then(async response => {
|
||||
console.log('GlobalQuota: Respuesta del endpoint /status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!text) {
|
||||
throw new Error('Empty response');
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(text);
|
||||
console.log('GlobalQuota: Datos recibidos de /status:', data);
|
||||
return data;
|
||||
} catch (parseError) {
|
||||
console.warn('GlobalQuota: respuesta no-JSON', text);
|
||||
throw new Error('Invalid JSON response');
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data && typeof data.used !== 'undefined' && typeof data.total !== 'undefined') {
|
||||
processQuotaData(data);
|
||||
} else {
|
||||
throw new Error('Invalid data structure');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('GlobalQuota: /status failed, trying /quota endpoint...', error.message);
|
||||
|
||||
// Fallback to legacy /quota endpoint
|
||||
return fetch(OC.generateUrl('/apps/globalquota/quota'))
|
||||
.then(response => {
|
||||
console.log('GlobalQuota: Respuesta del endpoint /quota:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('GlobalQuota: Datos recibidos de /quota:', data);
|
||||
|
||||
if (data && typeof data.used !== 'undefined' && typeof data.total !== 'undefined') {
|
||||
processQuotaData(data);
|
||||
} else {
|
||||
throw new Error('Invalid quota data structure');
|
||||
}
|
||||
})
|
||||
.catch(fallbackError => {
|
||||
console.error('GlobalQuota: Both endpoints failed:', fallbackError);
|
||||
showError('Failed to load quota data from both endpoints');
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
dataLoaded = false;
|
||||
});
|
||||
}
|
||||
|
||||
function processQuotaData(data) {
|
||||
// Unified calculation logic for both chart and stats
|
||||
const used = Number(data.used) || 0;
|
||||
const total = Number(data.total) || 0;
|
||||
const available = Number(data.available || data.free || 0);
|
||||
|
||||
// Calculate free space with unified logic
|
||||
let free;
|
||||
if (total > 0) {
|
||||
// If we have total, calculate free as total - used
|
||||
free = Math.max(0, total - used);
|
||||
} else if (available > 0) {
|
||||
// If no total but we have available, use that
|
||||
free = available;
|
||||
} else {
|
||||
// Fallback: assume no free space if we can't calculate
|
||||
free = 0;
|
||||
}
|
||||
|
||||
// Calculate percentage
|
||||
const percentage = total > 0 ? ((used / total) * 100) : 0;
|
||||
|
||||
// Create normalized data object
|
||||
const normalizedData = {
|
||||
used: used,
|
||||
free: free,
|
||||
total: total > 0 ? total : (used + free),
|
||||
percentage: percentage,
|
||||
formatted: data.formatted || {}
|
||||
};
|
||||
|
||||
console.log('GlobalQuota: Normalized data:', normalizedData);
|
||||
|
||||
// Update both chart and stats with the same data
|
||||
updateChart(normalizedData);
|
||||
updateStats(normalizedData);
|
||||
}
|
||||
|
||||
function updateChart(data) {
|
||||
if (!quotaChart || !chartInitialized) {
|
||||
console.warn('GlobalQuota: Chart not initialized, skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('GlobalQuota: Updating chart with:', {
|
||||
used: data.used,
|
||||
free: data.free,
|
||||
total: data.total
|
||||
});
|
||||
|
||||
quotaChart.data.datasets[0].data = [data.used, data.free];
|
||||
|
||||
// Update colors based on usage percentage
|
||||
let usedColor = '#2ecc71'; // Green
|
||||
|
||||
if (data.percentage > 90) {
|
||||
usedColor = '#e74c3c'; // Red
|
||||
} else if (data.percentage > 75) {
|
||||
usedColor = '#f39c12'; // Orange
|
||||
} else if (data.percentage > 50) {
|
||||
usedColor = '#f1c40f'; // Yellow
|
||||
}
|
||||
|
||||
quotaChart.data.datasets[0].backgroundColor = [usedColor, '#ecf0f1'];
|
||||
quotaChart.update('active');
|
||||
}
|
||||
|
||||
function updateStats(data) {
|
||||
// Use the same normalized data for stats
|
||||
const elems = {
|
||||
'quota-used': data.formatted?.used || formatBytes(data.used),
|
||||
'quota-free': data.formatted?.free || data.formatted?.available || formatBytes(data.free),
|
||||
'quota-total': data.formatted?.total || formatBytes(data.total),
|
||||
'quota-percentage': data.percentage.toFixed(1) + '%'
|
||||
};
|
||||
|
||||
console.log('GlobalQuota: Updating stats with:', elems);
|
||||
|
||||
Object.entries(elems).forEach(([id, val]) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.textContent = val;
|
||||
} else {
|
||||
console.warn(`GlobalQuota: Element #${id} not found`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes || isNaN(bytes) || bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const idx = Math.max(0, Math.min(i, sizes.length - 1));
|
||||
const num = bytes / Math.pow(k, idx);
|
||||
|
||||
return num.toFixed(2) + ' ' + sizes[idx];
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
console.error('GlobalQuota:', msg);
|
||||
|
||||
// Try to show error in dedicated error element
|
||||
const errorEl = document.getElementById('quota-error');
|
||||
if (errorEl) {
|
||||
errorEl.style.display = 'block';
|
||||
errorEl.textContent = msg;
|
||||
}
|
||||
|
||||
// Also try Nextcloud notification system
|
||||
try {
|
||||
if (OC && OC.Notification) {
|
||||
OC.Notification.showTemporary(msg, { type: 'error' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('GlobalQuota: Could not show notification:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function loadChartJS() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof Chart !== 'undefined') {
|
||||
console.log('GlobalQuota: Chart.js already available');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('GlobalQuota: Loading Chart.js...');
|
||||
|
||||
// Try to load from CDN first
|
||||
const cdnScript = document.createElement('script');
|
||||
cdnScript.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js';
|
||||
cdnScript.onload = function() {
|
||||
console.log('GlobalQuota: Chart.js loaded from CDN');
|
||||
resolve();
|
||||
};
|
||||
cdnScript.onerror = function() {
|
||||
console.warn('GlobalQuota: CDN failed, trying local Chart.js...');
|
||||
|
||||
// Fallback to local file
|
||||
const localScript = document.createElement('script');
|
||||
localScript.src = OC.filePath('globalquota', 'js', 'chart.min.js');
|
||||
localScript.onload = function() {
|
||||
console.log('GlobalQuota: Chart.js loaded locally');
|
||||
resolve();
|
||||
};
|
||||
localScript.onerror = function() {
|
||||
console.error('GlobalQuota: Failed to load Chart.js from both CDN and local');
|
||||
reject(new Error('Chart.js not available'));
|
||||
};
|
||||
document.head.appendChild(localScript);
|
||||
};
|
||||
document.head.appendChild(cdnScript);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('GlobalQuota: DOM loaded, iniciando...');
|
||||
|
||||
// Initialize chart if canvas exists
|
||||
const canvas = document.getElementById('globalquota-chart');
|
||||
if (canvas) {
|
||||
console.log('GlobalQuota: Canvas found, loading Chart.js...');
|
||||
|
||||
loadChartJS()
|
||||
.then(() => {
|
||||
initChart();
|
||||
// Load data only after chart is ready
|
||||
loadQuotaData();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('GlobalQuota: Failed to initialize chart:', error);
|
||||
showError('Chart visualization not available');
|
||||
// Still load data for stats even if chart fails
|
||||
loadQuotaData();
|
||||
});
|
||||
} else {
|
||||
console.warn('GlobalQuota: Canvas #globalquota-chart not found');
|
||||
// Load data for stats only
|
||||
loadQuotaData();
|
||||
}
|
||||
|
||||
// Set up refresh button
|
||||
const refreshBtn = document.getElementById('refresh-quota');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
console.log('GlobalQuota: Refresh button clicked');
|
||||
loadQuotaData();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\GlobalQuota\AppInfo;
|
||||
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCA\GlobalQuota\Listener\BeforeFileWrittenListener;
|
||||
use OCA\GlobalQuota\Listener\OverrideServerInfoListener;
|
||||
use OCP\Files\Events\Node\BeforeNodeWrittenEvent;
|
||||
|
||||
class Application extends App implements IBootstrap {
|
||||
public const APP_ID = 'globalquota';
|
||||
|
||||
public function __construct(array $urlParams = []) {
|
||||
parent::__construct(self::APP_ID, $urlParams);
|
||||
}
|
||||
|
||||
public function register(IRegistrationContext $context): void {
|
||||
// Registro del listener para bloqueo de uploads
|
||||
$context->registerEventListener(
|
||||
BeforeNodeWrittenEvent::class,
|
||||
BeforeFileWrittenListener::class
|
||||
);
|
||||
|
||||
// 🚀 DETECCIÓN AUTOMÁTICA: ServerInfo vs Panel Propio
|
||||
if (class_exists('\OCA\ServerInfo\Events\LoadAdditionalDataEvent')) {
|
||||
// Caso 1: ServerInfo soporta eventos → nos integramos
|
||||
$context->registerEventListener(
|
||||
\OCA\ServerInfo\Events\LoadAdditionalDataEvent::class,
|
||||
OverrideServerInfoListener::class
|
||||
);
|
||||
} else {
|
||||
// Caso 2: ServerInfo no soporta eventos → panel propio en Admin Settings
|
||||
$context->registerService('GlobalQuotaAdminSettings', function() {
|
||||
return new \OCA\GlobalQuota\Settings\Admin\Settings();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
// Boot logic if needed
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\GlobalQuota\Command;
|
||||
|
||||
use OCA\GlobalQuota\Service\QuotaService;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class RecalcCommand extends Command {
|
||||
private $quotaService;
|
||||
public function __construct(QuotaService $quotaService) {
|
||||
parent::__construct();
|
||||
$this->quotaService = $quotaService;
|
||||
}
|
||||
protected function configure() {
|
||||
$this->setName('globalquota:recalc')
|
||||
->setDescription('Recalculate global usage');
|
||||
}
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
$status = $this->quotaService->getStatus(true);
|
||||
$output->writeln("Recalculated usage: {$status['used_bytes']} bytes");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\GlobalQuota\Command;
|
||||
|
||||
use OCA\GlobalQuota\Service\QuotaService;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class SetQuotaCommand extends Command {
|
||||
private $quotaService;
|
||||
public function __construct(QuotaService $quotaService) {
|
||||
parent::__construct();
|
||||
$this->quotaService = $quotaService;
|
||||
}
|
||||
protected function configure() {
|
||||
$this->setName('globalquota:set')
|
||||
->setDescription('Set global quota in bytes')
|
||||
->addArgument('bytes', InputArgument::REQUIRED, 'Quota in bytes');
|
||||
}
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
$bytes = (int)$input->getArgument('bytes');
|
||||
$this->quotaService->setQuota($bytes);
|
||||
$output->writeln("Global quota set to $bytes bytes");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\GlobalQuota\Command;
|
||||
|
||||
use OCA\GlobalQuota\Service\QuotaService;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class StatusCommand extends Command {
|
||||
private $quotaService;
|
||||
public function __construct(QuotaService $quotaService) {
|
||||
parent::__construct();
|
||||
$this->quotaService = $quotaService;
|
||||
}
|
||||
protected function configure() {
|
||||
$this->setName('globalquota:status')
|
||||
->setDescription('Show global quota and usage');
|
||||
}
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
$status = $this->quotaService->getStatus();
|
||||
$output->writeln("Quota: {$status['quota_bytes']} bytes");
|
||||
$output->writeln("Used: {$status['used_bytes']} bytes");
|
||||
$output->writeln("Free: {$status['free_bytes']} bytes");
|
||||
$output->writeln("Usage: {$status['usage_percentage']}%");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\GlobalQuota\Controller;
|
||||
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCA\GlobalQuota\Service\QuotaService;
|
||||
use OCP\IRequest;
|
||||
|
||||
class ApiController extends Controller {
|
||||
private $quotaService;
|
||||
|
||||
public function __construct($AppName, IRequest $request, QuotaService $quotaService) {
|
||||
parent::__construct($AppName, $request);
|
||||
$this->quotaService = $quotaService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoCSRFRequired
|
||||
* @AdminRequired
|
||||
*/
|
||||
public function status(): DataResponse {
|
||||
return new DataResponse($this->quotaService->getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoCSRFRequired
|
||||
* @AdminRequired
|
||||
*/
|
||||
public function setQuota(): DataResponse {
|
||||
$data = $this->request->getParams();
|
||||
if (!isset($data['quota_bytes'])) {
|
||||
return new DataResponse(['error' => 'Missing quota_bytes'], 400);
|
||||
}
|
||||
$this->quotaService->setQuota((int)$data['quota_bytes']);
|
||||
return new DataResponse(['status' => 'ok']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\GlobalQuota\Controller;
|
||||
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\IRequest;
|
||||
use OCA\GlobalQuota\Service\QuotaService;
|
||||
|
||||
class QuotaController extends Controller {
|
||||
private QuotaService $quotaService;
|
||||
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
QuotaService $quotaService
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->quotaService = $quotaService;
|
||||
}
|
||||
|
||||
/** @NoAdminRequired @NoCSRFRequired */
|
||||
public function status(): JSONResponse {
|
||||
try {
|
||||
$status = $this->quotaService->getStatus();
|
||||
|
||||
return new JSONResponse([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'used' => $status['used_bytes'],
|
||||
'total' => $status['quota_bytes'],
|
||||
'free' => $status['free_bytes'],
|
||||
'percentage' => $status['usage_percentage'],
|
||||
'formatted' => [
|
||||
'used' => $this->formatBytes($status['used_bytes']),
|
||||
'total' => $this->formatBytes($status['quota_bytes']),
|
||||
'free' => $this->formatBytes($status['free_bytes'])
|
||||
]
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/** @NoAdminRequired @NoCSRFRequired */
|
||||
public function apiStatus(): JSONResponse {
|
||||
return $this->status();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* Endpoint for frontend quota display - returns simplified format
|
||||
*/
|
||||
public function getQuota(): JSONResponse {
|
||||
try {
|
||||
$status = $this->quotaService->getStatus();
|
||||
|
||||
return new JSONResponse([
|
||||
'used' => $status['used_bytes'],
|
||||
'available' => $status['free_bytes'],
|
||||
'total' => $status['quota_bytes'],
|
||||
'percentage' => $status['usage_percentage'],
|
||||
'formatted' => [
|
||||
'used' => $this->formatBytes($status['used_bytes']),
|
||||
'available' => $this->formatBytes($status['free_bytes']),
|
||||
'total' => $this->formatBytes($status['quota_bytes'])
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @AdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* Recalculate quota usage
|
||||
*/
|
||||
public function recalc(): JSONResponse {
|
||||
try {
|
||||
// Force recalculation of quota usage
|
||||
$status = $this->quotaService->getStatus(true);
|
||||
|
||||
return new JSONResponse([
|
||||
'status' => 'success',
|
||||
'message' => 'Quota recalculated successfully',
|
||||
'data' => [
|
||||
'used' => $status['used_bytes'],
|
||||
'total' => $status['quota_bytes'],
|
||||
'free' => $status['free_bytes'],
|
||||
'percentage' => $status['usage_percentage'],
|
||||
'formatted' => [
|
||||
'used' => $this->formatBytes($status['used_bytes']),
|
||||
'total' => $this->formatBytes($status['quota_bytes']),
|
||||
'free' => $this->formatBytes($status['free_bytes'])
|
||||
]
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse([
|
||||
'status' => 'error',
|
||||
'message' => 'Failed to recalculate quota: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/** @AdminRequired @NoCSRFRequired */
|
||||
public function updateQuota(): JSONResponse {
|
||||
$quotaBytes = $this->request->getParam('quota_bytes');
|
||||
if (!is_numeric($quotaBytes) || $quotaBytes < 0) {
|
||||
return new JSONResponse(['success' => false, 'error' => 'Invalid quota value'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->quotaService->setQuota((int)$quotaBytes);
|
||||
return new JSONResponse([
|
||||
'success' => true,
|
||||
'message' => 'Quota updated successfully',
|
||||
'data' => $this->quotaService->getStatus()
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @AdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* Set quota endpoint for REST API compatibility
|
||||
* Accepts both 'bytes' and 'quota_bytes' parameters
|
||||
*/
|
||||
public function setQuota(): JSONResponse {
|
||||
// Accept both 'bytes' and 'quota_bytes' for compatibility
|
||||
$quotaBytes = $this->request->getParam('bytes') ?? $this->request->getParam('quota_bytes');
|
||||
|
||||
if (!is_numeric($quotaBytes) || $quotaBytes < 0) {
|
||||
return new JSONResponse([
|
||||
'success' => false,
|
||||
'error' => 'Invalid quota value. Please provide a valid number in bytes parameter.'
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->quotaService->setQuota((int)$quotaBytes);
|
||||
$status = $this->quotaService->getStatus();
|
||||
|
||||
return new JSONResponse([
|
||||
'success' => true,
|
||||
'message' => 'Global quota set successfully',
|
||||
'data' => [
|
||||
'quota_set' => (int)$quotaBytes,
|
||||
'quota_formatted' => $this->formatBytes((int)$quotaBytes),
|
||||
'current_status' => [
|
||||
'used' => $status['used_bytes'],
|
||||
'total' => $status['quota_bytes'],
|
||||
'free' => $status['free_bytes'],
|
||||
'percentage' => $status['usage_percentage'],
|
||||
'formatted' => [
|
||||
'used' => $this->formatBytes($status['used_bytes']),
|
||||
'total' => $this->formatBytes($status['quota_bytes']),
|
||||
'free' => $this->formatBytes($status['free_bytes'])
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse([
|
||||
'success' => false,
|
||||
'error' => 'Failed to set quota: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes): string {
|
||||
$units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\GlobalQuota\Listener;
|
||||
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\Events\Node\BeforeFileCreatedEvent;
|
||||
use OCP\Files\Events\Node\BeforeFileUpdatedEvent;
|
||||
use OCP\Files\ForbiddenException;
|
||||
use OCA\GlobalQuota\Service\QuotaService;
|
||||
|
||||
class GlobalQuotaListener implements IEventListener {
|
||||
private $quotaService;
|
||||
|
||||
public function __construct(QuotaService $quotaService) {
|
||||
$this->quotaService = $quotaService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja eventos de creación/actualización de archivos
|
||||
* Bloquea uploads si exceden la cuota global
|
||||
*/
|
||||
public function handle($event): void {
|
||||
if ($event instanceof BeforeFileCreatedEvent || $event instanceof BeforeFileUpdatedEvent) {
|
||||
$quota = $this->quotaService->getQuota();
|
||||
|
||||
// Si no hay cuota global definida → no bloquea
|
||||
if ($quota === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tamaño actual del sistema
|
||||
$used = $this->quotaService->getUsage();
|
||||
|
||||
// Tamaño esperado del nuevo archivo
|
||||
$newSize = 0;
|
||||
if (method_exists($event, 'getSize')) {
|
||||
$newSize = $event->getSize();
|
||||
}
|
||||
|
||||
// Si el archivo ya existía (update), restar su tamaño actual
|
||||
$currentSize = 0;
|
||||
if ($event instanceof BeforeFileUpdatedEvent) {
|
||||
try {
|
||||
$node = $event->getNode();
|
||||
if ($node && method_exists($node, 'getSize')) {
|
||||
$currentSize = $node->getSize();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$currentSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Diferencia neta que sumará al uso total
|
||||
$delta = max(0, $newSize - $currentSize);
|
||||
|
||||
// Verificar si excede la cuota
|
||||
if (($used + $delta) > $quota) {
|
||||
throw new ForbiddenException("Global quota exceeded. Upload blocked.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\GlobalQuota\Listener;
|
||||
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCA\GlobalQuota\Service\QuotaService;
|
||||
use OCA\ServerInfo\Events\LoadAdditionalDataEvent;
|
||||
|
||||
class OverrideServerInfoListener implements IEventListener {
|
||||
private QuotaService $quotaService;
|
||||
|
||||
public function __construct(QuotaService $quotaService) {
|
||||
$this->quotaService = $quotaService;
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof LoadAdditionalDataEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$status = $this->quotaService->getStatus();
|
||||
|
||||
$event->addData('disk', [
|
||||
'used' => $status['used_bytes'],
|
||||
'available' => $status['free_bytes'],
|
||||
'total' => $status['quota_bytes'],
|
||||
'percent' => $status['percentage_used'],
|
||||
'mount' => 'GlobalQuota',
|
||||
'filesystem' => 'Global Storage'
|
||||
]);
|
||||
|
||||
$event->addData('quota_used', $status['used_bytes']);
|
||||
$event->addData('quota_total', $status['quota_bytes']);
|
||||
$event->addData('quota_free', $status['free_bytes']);
|
||||
$event->addData('quota_percentage', $status['percentage_used']);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\OC::$server->getLogger()->error(
|
||||
'GlobalQuota: Error al obtener datos para ServerInfo: ' . $e->getMessage(),
|
||||
['app' => 'globalquota']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\GlobalQuota\Service;
|
||||
|
||||
use OCP\IConfig;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
|
||||
class QuotaService {
|
||||
private $config;
|
||||
private $userManager;
|
||||
private $rootFolder;
|
||||
private $timeFactory;
|
||||
private $cacheKey = 'globalquota_usage';
|
||||
private $cacheTTL = 300; // 5 minutos
|
||||
|
||||
public function __construct(
|
||||
IConfig $config,
|
||||
IUserManager $userManager,
|
||||
IRootFolder $rootFolder,
|
||||
ITimeFactory $timeFactory
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->userManager = $userManager;
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->timeFactory = $timeFactory;
|
||||
}
|
||||
|
||||
public function getQuota(): ?int {
|
||||
$val = $this->config->getSystemValue('globalquota', []);
|
||||
return $val['quota_bytes'] ?? null;
|
||||
}
|
||||
|
||||
public function setQuota(int $bytes): void {
|
||||
$this->config->setSystemValue('globalquota', [
|
||||
'quota_bytes' => $bytes
|
||||
]);
|
||||
$this->config->deleteAppValue('globalquota', $this->cacheKey);
|
||||
}
|
||||
|
||||
public function getUsage(bool $forceRecalc = false): int {
|
||||
if (!$forceRecalc) {
|
||||
$cached = $this->getCache();
|
||||
if ($cached !== null) {
|
||||
return $cached['used'];
|
||||
}
|
||||
}
|
||||
|
||||
$totalUsed = 0;
|
||||
foreach ($this->userManager->search('') as $user) {
|
||||
try {
|
||||
$folder = $this->rootFolder->getUserFolder($user->getUID());
|
||||
// Usar getSize() en lugar de getCache()->getUsedSpace()
|
||||
$totalUsed += $folder->getSize();
|
||||
} catch (\Exception $e) {
|
||||
// Skip user if there's an error accessing their folder
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setCache($totalUsed);
|
||||
return $totalUsed;
|
||||
}
|
||||
|
||||
public function getStatus(bool $forceRecalc = false): array {
|
||||
$quota = $this->getQuota();
|
||||
$used = $this->getUsage($forceRecalc);
|
||||
return [
|
||||
'quota_bytes' => $quota,
|
||||
'used_bytes' => $used,
|
||||
'free_bytes' => $quota - $used,
|
||||
'usage_percentage' => $quota > 0 ? round(($used / $quota) * 100, 2) : 0
|
||||
];
|
||||
}
|
||||
|
||||
public function recalculateUsage(): array {
|
||||
// Fuerza el recálculo (sin cache)
|
||||
$status = $this->getStatus(true);
|
||||
|
||||
// Borra el cache anterior y guarda el nuevo
|
||||
$this->config->deleteAppValue('globalquota', $this->cacheKey);
|
||||
$this->setCache($status['used_bytes']);
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
private function getCache(): ?array {
|
||||
$raw = $this->config->getAppValue('globalquota', $this->cacheKey, null);
|
||||
if ($raw === null) return null;
|
||||
$data = json_decode($raw, true);
|
||||
if (($this->timeFactory->getTime() - $data['timestamp']) > $this->cacheTTL) {
|
||||
return null;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function setCache(int $used): void {
|
||||
$data = [
|
||||
'used' => $used,
|
||||
'timestamp' => $this->timeFactory->getTime()
|
||||
];
|
||||
$this->config->setAppValue('globalquota', $this->cacheKey, json_encode($data));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\GlobalQuota\Settings;
|
||||
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\Settings\ISettings;
|
||||
use OCP\Util;
|
||||
|
||||
class Admin implements ISettings {
|
||||
|
||||
/** @var IL10N */
|
||||
private $l10n;
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
|
||||
public function __construct(IL10N $l10n, IConfig $config) {
|
||||
$this->l10n = $l10n;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getForm(): TemplateResponse {
|
||||
Util::addScript('globalquota', 'admin-globalquota');
|
||||
Util::addStyle('globalquota', 'admin-globalquota');
|
||||
|
||||
return new TemplateResponse('globalquota', 'admin', [
|
||||
// aquí puedes pasar valores iniciales si hiciera falta
|
||||
], 'blank');
|
||||
}
|
||||
|
||||
public function getSection(): string {
|
||||
return 'server';
|
||||
}
|
||||
|
||||
public function getPriority(): int {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\GlobalQuota\Settings\Admin;
|
||||
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Settings\IIconSection;
|
||||
|
||||
class Section implements IIconSection {
|
||||
private IL10N $l;
|
||||
private IURLGenerator $urlGenerator;
|
||||
|
||||
public function __construct(IL10N $l, IURLGenerator $urlGenerator) {
|
||||
$this->l = $l;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
public function getID(): string {
|
||||
return 'globalquota';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->l->t('Global Quota');
|
||||
}
|
||||
|
||||
public function getPriority(): int {
|
||||
return 75;
|
||||
}
|
||||
|
||||
public function getIcon(): string {
|
||||
return $this->urlGenerator->imagePath('globalquota', 'app-dark.svg');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\GlobalQuota\Settings\Admin;
|
||||
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\Settings\ISettings;
|
||||
use OCA\GlobalQuota\Service\QuotaService;
|
||||
|
||||
class Settings implements ISettings {
|
||||
private IConfig $config;
|
||||
private IL10N $l;
|
||||
private QuotaService $quotaService;
|
||||
|
||||
public function __construct(IConfig $config, IL10N $l, QuotaService $quotaService) {
|
||||
$this->config = $config;
|
||||
$this->l = $l;
|
||||
$this->quotaService = $quotaService;
|
||||
}
|
||||
|
||||
public function getForm(): TemplateResponse {
|
||||
$showOwnChart = !class_exists('\OCA\ServerInfo\Events\LoadAdditionalDataEvent');
|
||||
try {
|
||||
$status = $this->quotaService->getStatus();
|
||||
} catch (\Exception $e) {
|
||||
$status = ['used_bytes' => 0,'quota_bytes' => 0,'free_bytes' => 0,'percentage_used' => 0];
|
||||
}
|
||||
return new TemplateResponse('globalquota','admin-settings',[
|
||||
'showChart' => $showOwnChart,
|
||||
'quotaStatus' => $status,
|
||||
'serverInfoIntegration' => !$showOwnChart
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSection(): string {
|
||||
return 'globalquota';
|
||||
}
|
||||
|
||||
public function getPriority(): int {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\GlobalQuota\SystemInfo;
|
||||
|
||||
use OCA\GlobalQuota\Service\QuotaService;
|
||||
use OCP\System\ISystemInfo;
|
||||
|
||||
class GlobalQuotaSystemInfo implements ISystemInfo {
|
||||
private $quotaService;
|
||||
|
||||
public function __construct(QuotaService $quotaService) {
|
||||
$this->quotaService = $quotaService;
|
||||
}
|
||||
|
||||
public function getDiskInfo(): array {
|
||||
return $this->quotaService->getStatus();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
script('globalquota', 'admin-settings');
|
||||
style('globalquota', 'admin-settings');
|
||||
?>
|
||||
|
||||
<div id="globalquota-settings" class="section">
|
||||
<h2 class="inlineblock"><?php p($l->t('Global Quota')); ?></h2>
|
||||
|
||||
<?php if ($_['serverInfoIntegration']): ?>
|
||||
<div class="globalquota-info">
|
||||
<p><?php p($l->t('GlobalQuota is integrated with Server Info.')); ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="globalquota-chart-container">
|
||||
<h3><?php p($l->t('Storage Usage')); ?></h3>
|
||||
|
||||
<!-- Error display element -->
|
||||
<div id="quota-error" style="display: none; color: #e74c3c; background: #fdf2f2; padding: 10px; border-radius: 4px; margin: 10px 0; border: 1px solid #fecaca;"></div>
|
||||
|
||||
<div class="globalquota-chart-wrapper">
|
||||
<canvas id="globalquota-chart" width="300" height="300"></canvas>
|
||||
<div class="globalquota-stats">
|
||||
<div class="stat-item"><span><?php p($l->t('Used:')); ?></span><span id="quota-used">-</span></div>
|
||||
<div class="stat-item"><span><?php p($l->t('Free:')); ?></span><span id="quota-free">-</span></div>
|
||||
<div class="stat-item"><span><?php p($l->t('Total:')); ?></span><span id="quota-total">-</span></div>
|
||||
<div class="stat-item"><span><?php p($l->t('Usage:')); ?></span><span id="quota-percentage">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="globalquota-actions">
|
||||
<button id="refresh-quota" class="button"><?php p($l->t('Refresh')); ?></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
script('globalquota', 'admin-globalquota'); // registra js/admin-globalquota.js
|
||||
style('globalquota', 'admin-globalquota'); // registra css/admin-globalquota.css
|
||||
?>
|
||||
|
||||
<div id="globalquota" class="section">
|
||||
<h2><?php p($l->t('Global Quota')); ?></h2>
|
||||
|
||||
<div class="globalquota-container">
|
||||
<div class="globalquota-chart-wrapper">
|
||||
<canvas id="globalquota-chart" width="240" height="240" aria-label="<?php p($l->t('Global quota donut chart')); ?>"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="globalquota-stats">
|
||||
<h3><?php p($l->t('Storage Usage')); ?></h3>
|
||||
<ul class="globalquota-list">
|
||||
<li>
|
||||
<strong><?php p($l->t('Used')); ?>:</strong>
|
||||
<span id="quota-used">-</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong><?php p($l->t('Free')); ?>:</strong>
|
||||
<span id="quota-free">-</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong><?php p($l->t('Total')); ?>:</strong>
|
||||
<span id="quota-total">-</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong><?php p($l->t('Usage')); ?>:</strong>
|
||||
<span id="quota-percentage">-</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button id="refresh-quota" class="button"><?php p($l->t('Refresh')); ?></button>
|
||||
<div id="quota-error" class="globalquota-error" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cargar Chart.js por CDN para asegurar disponibilidad -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
Loading…
Reference in New Issue