From a5f48de7d6c1a548539d7008ea467b0dc0929a7e Mon Sep 17 00:00:00 2001 From: chrislu Date: Fri, 4 Jul 2025 13:11:43 -0700 Subject: [PATCH] add vacuum operation --- weed/admin/dash/admin_server.go | 12 ++ weed/admin/handlers/admin_handlers.go | 12 ++ weed/admin/handlers/cluster_handlers.go | 32 ++++ weed/admin/view/app/cluster_volumes.templ | 76 +++++++- weed/admin/view/app/cluster_volumes_templ.go | 180 +++++++++++-------- weed/admin/view/app/volume_details.templ | 80 ++++++++- weed/admin/view/app/volume_details_templ.go | 34 +++- 7 files changed, 342 insertions(+), 84 deletions(-) diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index 89bae8ea2..93b556af8 100644 --- a/weed/admin/dash/admin_server.go +++ b/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 + }) +} diff --git a/weed/admin/handlers/admin_handlers.go b/weed/admin/handlers/admin_handlers.go index 012694ffb..541bb6293 100644 --- a/weed/admin/handlers/admin_handlers.go +++ b/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) + } } } } diff --git a/weed/admin/handlers/cluster_handlers.go b/weed/admin/handlers/cluster_handlers.go index 769cc2894..d8378e690 100644 --- a/weed/admin/handlers/cluster_handlers.go +++ b/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, + }) +} diff --git a/weed/admin/view/app/cluster_volumes.templ b/weed/admin/view/app/cluster_volumes.templ index cc1179d47..f9b661e2b 100644 --- a/weed/admin/view/app/cluster_volumes.templ +++ b/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)}> - @@ -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 = ''; + + // 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} + + `; + + document.body.appendChild(toast); + + // Auto-remove after 5 seconds + setTimeout(() => { + if (toast.parentNode) { + toast.parentNode.removeChild(toast); + } + }, 5000); + } } diff --git a/weed/admin/view/app/cluster_volumes_templ.go b/weed/admin/view/app/cluster_volumes_templ.go index a68ed5820..51f3dd0cf 100644 --- a/weed/admin/view/app/cluster_volumes_templ.go +++ b/weed/admin/view/app/cluster_volumes_templ.go @@ -675,228 +675,254 @@ func ClusterVolumes(data dash.ClusterVolumesData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\"> ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "
Showing ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "
Showing ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", (data.CurrentPage-1)*data.PageSize+1)) + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", (data.CurrentPage-1)*data.PageSize+1)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 383, Col: 98} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 385, Col: 98} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, " to ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " to ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", minInt(data.CurrentPage*data.PageSize, data.TotalVolumes))) + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", minInt(data.CurrentPage*data.PageSize, data.TotalVolumes))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 383, Col: 180} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 385, Col: 180} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, " of ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, " of ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalVolumes)) + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalVolumes)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 383, Col: 222} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 385, Col: 222} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " volumes
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, " volumes
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.TotalPages > 1 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "
Page ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "
Page ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage)) + var templ_7745c5c3_Var34 string + templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 389, Col: 77} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 391, Col: 77} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, " of ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, " of ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var33 string - templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalPages)) + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalPages)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 389, Col: 117} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 391, Col: 117} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.TotalPages > 1 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "
No Volumes Found

No volumes are currently available in the cluster.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "
No Volumes Found

No volumes are currently available in the cluster.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "
Last updated: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "
Last updated: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var39 string - templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) + var templ_7745c5c3_Var41 string + templ_7745c5c3_Var41, 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/cluster_volumes.templ`, Line: 461, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 463, Col: 81} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) + _, 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, 103, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -955,23 +981,23 @@ func getSortIcon(column, currentSort, currentOrder string) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var40 := templ.GetChildren(ctx) - if templ_7745c5c3_Var40 == nil { - templ_7745c5c3_Var40 = templ.NopComponent + templ_7745c5c3_Var42 := templ.GetChildren(ctx) + if templ_7745c5c3_Var42 == nil { + templ_7745c5c3_Var42 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if column != currentSort { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if currentOrder == "asc" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/volume_details.templ b/weed/admin/view/app/volume_details.templ index 7c192f429..16c8349f5 100644 --- a/weed/admin/view/app/volume_details.templ +++ b/weed/admin/view/app/volume_details.templ @@ -355,7 +355,10 @@ templ VolumeDetails(data dash.VolumeDetailsData) {
-
@@ -379,6 +382,81 @@ templ VolumeDetails(data dash.VolumeDetailsData) {
+ + + } func formatTimestamp(unixTimestamp int64) string { diff --git a/weed/admin/view/app/volume_details_templ.go b/weed/admin/view/app/volume_details_templ.go index 2764e7893..f6b03dfc9 100644 --- a/weed/admin/view/app/volume_details_templ.go +++ b/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
Use these actions to perform maintenance operations on the volume.
Last updated: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "
Actions
") + 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, "\">Vacuum
Use these actions to perform maintenance operations on the volume.
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, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }