feat: 0.11.7 — Stale data cleanup + FileBrowser sync after migration + deploy title fix
- Detect stale data on non-active storage paths after migration; show on deploy/settings page with size info and two-step delete confirmation - Add POST /api/storage/stale-cleanup handler with safety checks (active path protection, registered-path validation, ProtectedHDDPaths guard) - Export ProtectedHDDPaths() from stacks package for reuse in web handlers - Sync FileBrowser mounts after successful app data migration - Deploy page title/h2 now shows "Beállítások" for already-deployed apps instead of always showing "Telepítés" - Also add delete-old-data button on migration-done card in migrate.html Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -140,6 +140,8 @@ func (s *Server) storageAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.storageMigrateAPIHandler(w, r)
|
||||
case path == "/api/storage/migrate/status" && r.Method == http.MethodGet:
|
||||
s.storageMigrateStatusAPIHandler(w, r)
|
||||
case path == "/api/storage/stale-cleanup" && r.Method == http.MethodPost:
|
||||
s.staleDataCleanupHandler(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
@@ -452,6 +454,8 @@ func (s *Server) storageMigrateAPIHandler(w http.ResponseWriter, r *http.Request
|
||||
s.logger.Printf("[ERROR] Migration failed: stack=%s: %v", req.StackName, err)
|
||||
} else {
|
||||
s.logger.Printf("[INFO] Migration complete: stack=%s → %s", req.StackName, req.TargetPath)
|
||||
// Sync FileBrowser mounts (storage paths may now have new app data)
|
||||
go s.syncFileBrowserMounts()
|
||||
}
|
||||
close(progressCh)
|
||||
}()
|
||||
@@ -619,3 +623,189 @@ func (s *Server) storageLabelForPath(path string) string {
|
||||
}
|
||||
return strings.TrimPrefix(path, "/mnt/")
|
||||
}
|
||||
|
||||
// StaleStorageData describes leftover data on a non-active storage path.
|
||||
type StaleStorageData struct {
|
||||
Path string // e.g., "/mnt/hdd_placeholder"
|
||||
Label string // e.g., "Külső tárhely (hdd_placeholder)"
|
||||
Mounts []string // host-side paths with data
|
||||
SizeHuman string // e.g., "48 MB"
|
||||
SizeBytes int64
|
||||
}
|
||||
|
||||
// findStaleStorageData detects leftover app data on non-active storage paths.
|
||||
// This happens after migration: the old data stays on the previous storage path.
|
||||
func (s *Server) findStaleStorageData(stackName string) []StaleStorageData {
|
||||
appCfg := s.stackMgr.LoadAppConfigByName(stackName)
|
||||
if appCfg == nil {
|
||||
return nil
|
||||
}
|
||||
currentHDDPath := appCfg.Env["HDD_PATH"]
|
||||
if currentHDDPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
stack, ok := s.stackMgr.GetStack(stackName)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []StaleStorageData
|
||||
|
||||
// Check all registered storage paths except the current one
|
||||
for _, sp := range s.settings.GetStoragePaths() {
|
||||
if sp.Path == currentHDDPath {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use ParseComposeHDDMounts to find what dirs WOULD exist on this path
|
||||
mounts := stacks.ParseComposeHDDMounts(stack.ComposePath, sp.Path)
|
||||
if len(mounts) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check which mounts actually have data
|
||||
var existingMounts []string
|
||||
var totalSize int64
|
||||
for _, m := range mounts {
|
||||
info, err := os.Stat(m)
|
||||
if err != nil || !info.IsDir() {
|
||||
continue
|
||||
}
|
||||
size := dirSizeInt64(m)
|
||||
if size > 0 {
|
||||
existingMounts = append(existingMounts, m)
|
||||
totalSize += size
|
||||
}
|
||||
}
|
||||
|
||||
if len(existingMounts) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
label := sp.Label
|
||||
if label == "" {
|
||||
label = settings.InferStorageLabel(sp.Path)
|
||||
}
|
||||
|
||||
result = append(result, StaleStorageData{
|
||||
Path: sp.Path,
|
||||
Label: label,
|
||||
Mounts: existingMounts,
|
||||
SizeHuman: dirSizeBytesHuman(totalSize),
|
||||
SizeBytes: totalSize,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// staleDataCleanupHandler handles POST /api/storage/stale-cleanup.
|
||||
// Deletes leftover app data from a previous storage path after migration.
|
||||
func (s *Server) staleDataCleanupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
StackName string `json:"stack_name"`
|
||||
StalePath string `json:"stale_path"` // the old storage root, e.g., "/mnt/hdd_placeholder"
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonError(w, "Érvénytelen kérés", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.StackName == "" || req.StalePath == "" {
|
||||
jsonError(w, "Hiányos paraméterek", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the app exists and is deployed
|
||||
stack, ok := s.stackMgr.GetStack(req.StackName)
|
||||
if !ok {
|
||||
jsonError(w, "Alkalmazás nem található: "+req.StackName, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
appCfg := s.stackMgr.LoadAppConfigByName(req.StackName)
|
||||
if appCfg == nil || !appCfg.Deployed {
|
||||
jsonError(w, "Az alkalmazás nincs telepítve", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
currentHDDPath := appCfg.Env["HDD_PATH"]
|
||||
if currentHDDPath == "" {
|
||||
jsonError(w, "Az alkalmazásnak nincs HDD_PATH beállítva", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// SAFETY: StalePath must NOT be the current HDD_PATH
|
||||
if req.StalePath == currentHDDPath {
|
||||
jsonError(w, "Az aktív tárhely adatai nem törölhetők! Ez az alkalmazás aktuális adattárolója.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// SAFETY: StalePath must be a registered storage path
|
||||
found := false
|
||||
for _, sp := range s.settings.GetStoragePaths() {
|
||||
if sp.Path == req.StalePath {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
jsonError(w, "A megadott útvonal nem regisztrált adattároló", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Find mounts to delete
|
||||
mounts := stacks.ParseComposeHDDMounts(stack.ComposePath, req.StalePath)
|
||||
if len(mounts) == 0 {
|
||||
jsonError(w, "Nem találhatók törlendő adatok", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Protected paths check
|
||||
protected := stacks.ProtectedHDDPaths(req.StalePath)
|
||||
|
||||
var deleted []string
|
||||
var errors []string
|
||||
var totalFreed int64
|
||||
|
||||
for _, mountPath := range mounts {
|
||||
cleanPath := filepath.Clean(mountPath)
|
||||
|
||||
// Safety: never delete protected top-level dirs
|
||||
if protected != nil && protected[cleanPath] {
|
||||
s.logger.Printf("[WARN] Refusing to delete protected HDD path: %s", cleanPath)
|
||||
errors = append(errors, fmt.Sprintf("Védett útvonal, nem törölhető: %s", cleanPath))
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify it actually exists and has data
|
||||
info, err := os.Stat(cleanPath)
|
||||
if err != nil || !info.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
size := dirSizeInt64(cleanPath)
|
||||
|
||||
if err := os.RemoveAll(cleanPath); err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to remove stale data %s: %v", cleanPath, err)
|
||||
errors = append(errors, fmt.Sprintf("Törlés sikertelen: %s — %v", cleanPath, err))
|
||||
} else {
|
||||
s.logger.Printf("[INFO] Removed stale data: %s (%s) for stack %s", cleanPath, dirSizeBytesHuman(size), req.StackName)
|
||||
deleted = append(deleted, cleanPath)
|
||||
totalFreed += size
|
||||
}
|
||||
}
|
||||
|
||||
if len(deleted) == 0 && len(errors) > 0 {
|
||||
jsonError(w, "Törlés sikertelen: "+strings.Join(errors, "; "), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
jsonResponse(w, map[string]interface{}{
|
||||
"ok": true,
|
||||
"deleted": deleted,
|
||||
"freed_human": dirSizeBytesHuman(totalFreed),
|
||||
"errors": errors,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user