mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2025-08-20 09:53:01 +08:00
add vacuum operation
This commit is contained in:
parent
0c1d4b2d08
commit
a5f48de7d6
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -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 {
|
||||
|
||||
@ -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…
Reference in New Issue
Block a user