F-S2 + F-S3: compose-derived appdata dir resolution (v0.131.0)
The controller assumed an app's HDD appdata dir is always appdata/<stackName>.
paperless-ngx writes appdata/paperless (stack paperless-ngx), so every consumer
keying by stack name silently missed it via a stat-and-skip. One canonical
resolver appbackup.AppDataDirNames derives the real dir name(s) from the app's
compose ${HDD_PATH} binds; all consumers use it.
- F-S2 (tier-2): RunTier2 mirrors the resolved appdata/<name> (paperless docs
got NO tier-2 copy before). Tier2Info size + RestoreTier2Files live dir use it.
WARN when a declared appdata dir is absent. New tier2Mirror seam.
- F-S3 (migrate, NEW): all six per-app appdata legs (collision/size/copy/verify/
cleanup/skip-set) now loop resolved names. scope="app" migration of paperless
previously copied nothing and left an empty media dir (scope="all" was saved by
the merge walk). WARN on missing declared dir in the copy leg.
- Multi-dir (N>1) refusal: tier-2 backup/info/restore refuse loudly (Hungarian);
migrate supports N. No catalog app hits it today; lifted by Task 3.
- Display: storage page sums resolved dirs.
- Truth repair: the v0.130.0 "tier-2 copies the namespace wholesale" claim is
false; corrected in CHANGELOG + main.go export-adapter comment.
+9 tests; red-proofs RP-1..RP-5 all confirmed. Controller-only, no agent/hub
coupling. Task 1 of the backup-classification-redesign arc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A45Qop8YY8tS94bz63LFne
This commit is contained in:
@@ -1675,18 +1675,28 @@ func (s *Server) appDetailsForPath(storagePath string) []StorageAppDetail {
|
||||
Name: stack.Meta.DisplayName,
|
||||
Stack: stack.Meta.Slug,
|
||||
}
|
||||
// Try to get data size from the storage subdirectory
|
||||
appDataDir := backup.AppDataDir(storagePath, stack.Name)
|
||||
if fi, err := os.Stat(appDataDir); err == nil && fi.IsDir() {
|
||||
detail.SizeHuman = dirSizeHuman(appDataDir)
|
||||
// Try to get data size from the storage subdirectory. F-S2: the app's real appdata dir name is
|
||||
// NOT always the stack name (paperless-ngx writes appdata/paperless) — sum the resolved dir(s).
|
||||
// Here hddPath == storagePath (the drive's in-guest mount is the namespace root, Model A).
|
||||
var total int64
|
||||
var any bool
|
||||
for _, name := range s.stackMgr.ResolveAppDataDirNames(stack.Name) {
|
||||
d := backup.AppDataDir(storagePath, name)
|
||||
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
|
||||
total += dirSizeBytesWalk(d)
|
||||
any = true
|
||||
}
|
||||
}
|
||||
if any {
|
||||
detail.SizeHuman = humanizeDirBytes(total)
|
||||
}
|
||||
details = append(details, detail)
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
// dirSizeHuman returns a human-readable size for a directory.
|
||||
func dirSizeHuman(path string) string {
|
||||
// dirSizeBytesWalk returns the total size in bytes of the regular files under path (0 if absent).
|
||||
func dirSizeBytesWalk(path string) int64 {
|
||||
var total int64
|
||||
filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
@@ -1695,6 +1705,11 @@ func dirSizeHuman(path string) string {
|
||||
total += info.Size()
|
||||
return nil
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
// humanizeDirBytes formats a byte count as B/KB/MB/GB (the storage-page display convention).
|
||||
func humanizeDirBytes(total int64) string {
|
||||
const (
|
||||
KB = 1024
|
||||
MB = KB * 1024
|
||||
@@ -1712,6 +1727,11 @@ func dirSizeHuman(path string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// dirSizeHuman returns a human-readable size for a single directory.
|
||||
func dirSizeHuman(path string) string {
|
||||
return humanizeDirBytes(dirSizeBytesWalk(path))
|
||||
}
|
||||
|
||||
func formatFreeSpace(gb float64) string {
|
||||
if gb >= 1000 {
|
||||
return fmt.Sprintf("%.1f TB", gb/1024)
|
||||
|
||||
Reference in New Issue
Block a user