Browse Source

add vacuum operation

pull/6943/head
chrislu 3 months ago
parent
commit
a5f48de7d6
  1. 12
      weed/admin/dash/admin_server.go
  2. 12
      weed/admin/handlers/admin_handlers.go
  3. 32
      weed/admin/handlers/cluster_handlers.go
  4. 76
      weed/admin/view/app/cluster_volumes.templ
  5. 180
      weed/admin/view/app/cluster_volumes_templ.go
  6. 80
      weed/admin/view/app/volume_details.templ
  7. 34
      weed/admin/view/app/volume_details_templ.go

12
weed/admin/dash/admin_server.go

@ -1389,3 +1389,15 @@ func (s *AdminServer) GetVolumeDetails(volumeID int, server string) (*VolumeDeta
LastUpdated: time.Now(),
}, nil
}
// VacuumVolume performs a vacuum operation on a specific volume
func (s *AdminServer) VacuumVolume(volumeID int, server string) error {
return s.WithMasterClient(func(client master_pb.SeaweedClient) error {
_, err := client.VacuumVolume(context.Background(), &master_pb.VacuumVolumeRequest{
VolumeId: uint32(volumeID),
GarbageThreshold: 0.0001, // A very low threshold to ensure all garbage is collected
Collection: "", // Empty for all collections
})
return err
})
}

12
weed/admin/handlers/admin_handlers.go

@ -112,6 +112,12 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username,
filesApi.GET("/view", h.fileBrowserHandlers.ViewFile)
filesApi.GET("/properties", h.fileBrowserHandlers.GetFileProperties)
}
// Volume management API routes
volumeApi := api.Group("/volumes")
{
volumeApi.POST("/:id/:server/vacuum", h.clusterHandlers.VacuumVolume)
}
}
} else {
// No authentication required - all routes are public
@ -177,6 +183,12 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username,
filesApi.GET("/view", h.fileBrowserHandlers.ViewFile)
filesApi.GET("/properties", h.fileBrowserHandlers.GetFileProperties)
}
// Volume management API routes
volumeApi := api.Group("/volumes")
{
volumeApi.POST("/:id/:server/vacuum", h.clusterHandlers.VacuumVolume)
}
}
}
}

32
weed/admin/handlers/cluster_handlers.go

@ -240,3 +240,35 @@ func (h *ClusterHandlers) GetVolumeServers(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{"volume_servers": topology.VolumeServers})
}
// VacuumVolume handles volume vacuum requests via API
func (h *ClusterHandlers) VacuumVolume(c *gin.Context) {
volumeIDStr := c.Param("id")
server := c.Param("server")
if volumeIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Volume ID is required"})
return
}
volumeID, err := strconv.Atoi(volumeIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid volume ID"})
return
}
// Perform vacuum operation
err = h.adminServer.VacuumVolume(volumeID, server)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to vacuum volume: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Volume vacuum started successfully",
"volume_id": volumeID,
"server": server,
})
}

76
weed/admin/view/app/cluster_volumes.templ

@ -364,8 +364,10 @@ templ ClusterVolumes(data dash.ClusterVolumesData) {
title="View Details" data-volume-id={fmt.Sprintf("%d", volume.Id)}>
<i class="fas fa-eye"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
title="Vacuum">
<button type="button" class="btn btn-outline-secondary btn-sm vacuum-btn"
title="Vacuum"
data-volume-id={fmt.Sprintf("%d", volume.Id)}
data-server={volume.Server}>
<i class="fas fa-compress-alt"></i>
</button>
</div>
@ -485,6 +487,16 @@ templ ClusterVolumes(data dash.ClusterVolumesData) {
viewVolumeDetails(volumeId);
});
});
// Add click handlers to vacuum buttons
document.querySelectorAll('.vacuum-btn').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const volumeId = this.getAttribute('data-volume-id');
const server = this.getAttribute('data-server');
performVacuum(volumeId, server, this);
});
});
});
function goToPage(page) {
@ -531,6 +543,66 @@ templ ClusterVolumes(data dash.ClusterVolumesData) {
window.location.href = `/cluster/volumes/${volumeId}/${encodeURIComponent(server)}`;
}
function performVacuum(volumeId, server, button) {
// Disable button and show loading state
const originalHTML = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
// Send vacuum request
fetch(`/api/volumes/${volumeId}/${encodeURIComponent(server)}/vacuum`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
showMessage(data.error, 'error');
} else {
showMessage(data.message || 'Volume vacuum started successfully', 'success');
// Optionally refresh the page after a delay to show updated vacuum status
setTimeout(() => {
window.location.reload();
}, 2000);
}
})
.catch(error => {
console.error('Error:', error);
showMessage('Failed to start vacuum operation', 'error');
})
.finally(() => {
// Re-enable button
button.disabled = false;
button.innerHTML = originalHTML;
});
}
function showMessage(message, type) {
// Create toast notification
const toast = document.createElement('div');
toast.className = `alert alert-${type === 'error' ? 'danger' : 'success'} alert-dismissible fade show position-fixed`;
toast.style.top = '20px';
toast.style.right = '20px';
toast.style.zIndex = '9999';
toast.style.minWidth = '300px';
toast.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(toast);
// Auto-remove after 5 seconds
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 5000);
}
</script>
}

180
weed/admin/view/app/cluster_volumes_templ.go
File diff suppressed because it is too large
View File

80
weed/admin/view/app/volume_details.templ

@ -355,7 +355,10 @@ templ VolumeDetails(data dash.VolumeDetailsData) {
</div>
<div class="card-body">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-danger" title="Vacuum Volume">
<button type="button" class="btn btn-outline-danger vacuum-btn"
title="Vacuum Volume"
data-volume-id={fmt.Sprintf("%d", data.Volume.Id)}
data-server={data.Volume.Server}>
<i class="fas fa-compress-alt me-1"></i>Vacuum
</button>
</div>
@ -379,6 +382,81 @@ templ VolumeDetails(data dash.VolumeDetailsData) {
</small>
</div>
</div>
<!-- JavaScript for volume actions -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add click handler for vacuum button
const vacuumBtn = document.querySelector('.vacuum-btn');
if (vacuumBtn) {
vacuumBtn.addEventListener('click', function() {
const volumeId = this.getAttribute('data-volume-id');
const server = this.getAttribute('data-server');
performVacuum(volumeId, server, this);
});
}
});
function performVacuum(volumeId, server, button) {
// Disable button and show loading state
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Vacuuming...';
// Send vacuum request
fetch(`/api/volumes/${volumeId}/${encodeURIComponent(server)}/vacuum`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
showMessage(data.error, 'error');
} else {
showMessage(data.message || 'Volume vacuum started successfully', 'success');
// Optionally refresh the page after a delay
setTimeout(() => {
window.location.reload();
}, 2000);
}
})
.catch(error => {
console.error('Error:', error);
showMessage('Failed to start vacuum operation', 'error');
})
.finally(() => {
// Re-enable button
button.disabled = false;
button.innerHTML = originalText;
});
}
function showMessage(message, type) {
// Create toast notification
const toast = document.createElement('div');
toast.className = `alert alert-${type === 'error' ? 'danger' : 'success'} alert-dismissible fade show position-fixed`;
toast.style.top = '20px';
toast.style.right = '20px';
toast.style.zIndex = '9999';
toast.style.minWidth = '300px';
toast.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(toast);
// Auto-remove after 5 seconds
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 5000);
}
</script>
}
func formatTimestamp(unixTimestamp int64) string {

34
weed/admin/view/app/volume_details_templ.go

@ -629,20 +629,46 @@ func VolumeDetails(data dash.VolumeDetailsData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<!-- Actions Card --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-tools me-2\"></i>Actions</h6></div><div class=\"card-body\"><div class=\"btn-group\" role=\"group\"><button type=\"button\" class=\"btn btn-outline-danger\" title=\"Vacuum Volume\"><i class=\"fas fa-compress-alt me-1\"></i>Vacuum</button></div><div class=\"mt-3\"><small class=\"text-muted\"><i class=\"fas fa-info-circle me-1\"></i> Use these actions to perform maintenance operations on the volume.</small></div></div></div></div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<!-- Actions Card --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-tools me-2\"></i>Actions</h6></div><div class=\"card-body\"><div class=\"btn-group\" role=\"group\"><button type=\"button\" class=\"btn btn-outline-danger vacuum-btn\" title=\"Vacuum Volume\" data-volume-id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05"))
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.Volume.Id))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 378, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 360, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</small></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" data-server=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var42 string
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volume.Server)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 361, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\"><i class=\"fas fa-compress-alt me-1\"></i>Vacuum</button></div><div class=\"mt-3\"><small class=\"text-muted\"><i class=\"fas fa-info-circle me-1\"></i> Use these actions to perform maintenance operations on the volume.</small></div></div></div></div></div><!-- Last Updated --><div class=\"row\"><div class=\"col-12\"><small class=\"text-muted\"><i class=\"fas fa-clock me-1\"></i> Last updated: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/volume_details.templ`, Line: 381, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</small></div></div><!-- JavaScript for volume actions --><script>\n document.addEventListener('DOMContentLoaded', function() {\n // Add click handler for vacuum button\n const vacuumBtn = document.querySelector('.vacuum-btn');\n if (vacuumBtn) {\n vacuumBtn.addEventListener('click', function() {\n const volumeId = this.getAttribute('data-volume-id');\n const server = this.getAttribute('data-server');\n performVacuum(volumeId, server, this);\n });\n }\n });\n\n function performVacuum(volumeId, server, button) {\n // Disable button and show loading state\n const originalText = button.innerHTML;\n button.disabled = true;\n button.innerHTML = '<i class=\"fas fa-spinner fa-spin me-1\"></i>Vacuuming...';\n\n // Send vacuum request\n fetch(`/api/volumes/${volumeId}/${encodeURIComponent(server)}/vacuum`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n }\n })\n .then(response => response.json())\n .then(data => {\n if (data.error) {\n showMessage(data.error, 'error');\n } else {\n showMessage(data.message || 'Volume vacuum started successfully', 'success');\n // Optionally refresh the page after a delay\n setTimeout(() => {\n window.location.reload();\n }, 2000);\n }\n })\n .catch(error => {\n console.error('Error:', error);\n showMessage('Failed to start vacuum operation', 'error');\n })\n .finally(() => {\n // Re-enable button\n button.disabled = false;\n button.innerHTML = originalText;\n });\n }\n\n function showMessage(message, type) {\n // Create toast notification\n const toast = document.createElement('div');\n toast.className = `alert alert-${type === 'error' ? 'danger' : 'success'} alert-dismissible fade show position-fixed`;\n toast.style.top = '20px';\n toast.style.right = '20px';\n toast.style.zIndex = '9999';\n toast.style.minWidth = '300px';\n \n toast.innerHTML = `\n ${message}\n <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\"></button>\n `;\n \n document.body.appendChild(toast);\n \n // Auto-remove after 5 seconds\n setTimeout(() => {\n if (toast.parentNode) {\n toast.parentNode.removeChild(toast);\n }\n }, 5000);\n }\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

Loading…
Cancel
Save