v0.9.0: Storage paths registry, per-app HDD_PATH resolution, storage management UI
- Fix backup toggles not appearing (read each app's own HDD_PATH from app.yaml) - Storage paths registry in settings.json with auto-discovery from deployed apps - Settings page "Adattárolók" section with disk usage, add/remove/default/schedulable - Deploy page path field as dropdown of registered storage paths - Health check storage monitoring (mount point, disk usage alerts) - Mount-point validation utilities (Linux syscall + cross-platform stubs) - Controller docker-compose mount changed to /mnt:/mnt:rw for multi-storage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
//go:build linux
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// IsMountPoint checks if a path is on a different device than its parent.
|
||||
// Returns true if the path is a mount point (different device ID from parent).
|
||||
func IsMountPoint(path string) bool {
|
||||
var pathStat, parentStat syscall.Stat_t
|
||||
if err := syscall.Stat(path, &pathStat); err != nil {
|
||||
return false
|
||||
}
|
||||
parent := filepath.Dir(path)
|
||||
if err := syscall.Stat(parent, &parentStat); err != nil {
|
||||
return false
|
||||
}
|
||||
return pathStat.Dev != parentStat.Dev
|
||||
}
|
||||
|
||||
// IsWritable checks if the given path is writable by attempting to create+remove a temp file.
|
||||
func IsWritable(path string) bool {
|
||||
testFile := filepath.Join(path, ".felhom-write-test")
|
||||
f, err := os.Create(testFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
f.Close()
|
||||
os.Remove(testFile)
|
||||
return true
|
||||
}
|
||||
|
||||
// PathsOverlap returns true if one path is a parent or child of the other.
|
||||
func PathsOverlap(a, b string) bool {
|
||||
a = filepath.Clean(a)
|
||||
b = filepath.Clean(b)
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
aSep := a + string(os.PathSeparator)
|
||||
bSep := b + string(os.PathSeparator)
|
||||
return strings.HasPrefix(aSep, bSep) || strings.HasPrefix(bSep, aSep)
|
||||
}
|
||||
|
||||
// DiskUsageInfo holds disk usage statistics for a path.
|
||||
type DiskUsageInfo struct {
|
||||
TotalGB float64
|
||||
UsedGB float64
|
||||
AvailGB float64
|
||||
UsedPercent float64
|
||||
TotalHuman string
|
||||
UsedHuman string
|
||||
}
|
||||
|
||||
// GetDiskUsage returns disk usage info for a path, or nil on error.
|
||||
func GetDiskUsage(path string) *DiskUsageInfo {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
bsize := uint64(stat.Bsize)
|
||||
total := stat.Blocks * bsize
|
||||
avail := stat.Bavail * bsize
|
||||
used := total - (stat.Bfree * bsize)
|
||||
|
||||
const gb = 1024 * 1024 * 1024
|
||||
info := &DiskUsageInfo{
|
||||
TotalGB: float64(total) / float64(gb),
|
||||
UsedGB: float64(used) / float64(gb),
|
||||
AvailGB: float64(avail) / float64(gb),
|
||||
}
|
||||
if total > 0 {
|
||||
info.UsedPercent = float64(used) / float64(total) * 100
|
||||
}
|
||||
info.TotalHuman = formatGB(info.TotalGB)
|
||||
info.UsedHuman = formatGB(info.UsedGB)
|
||||
return info
|
||||
}
|
||||
|
||||
func formatGB(gb float64) string {
|
||||
if gb >= 1000 {
|
||||
return fmt.Sprintf("%.1f TB", gb/1024)
|
||||
}
|
||||
return fmt.Sprintf("%.1f GB", gb)
|
||||
}
|
||||
Reference in New Issue
Block a user