v0.288.0: the remove dialog and result name the household's userdata folders, which stay (decision 67, R-800)
gates / gates (push) Successful in 28s
gates / gates (push) Successful in 28s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -34,6 +34,9 @@ type DeleteResponse struct {
|
||||
HDDPathsPreserved []string `json:"hdd_paths_preserved"`
|
||||
HDDPathsMissing []string `json:"hdd_paths_missing,omitempty"`
|
||||
HDDNote string `json:"hdd_note,omitempty"`
|
||||
// UserdataKept (`09` §3 decision 67, R-800): the household's own folders the app binds through ${USERDATA_PATH}
|
||||
// that exist — never deleted by a remove; the page names them. Always non-nil.
|
||||
UserdataKept []string `json:"userdata_kept"`
|
||||
}
|
||||
|
||||
// RemoveResponse holds the result of removing a deployed (non-orphaned) stack. Same R-442 shape as
|
||||
@@ -46,6 +49,8 @@ type RemoveResponse struct {
|
||||
HDDPathsPreserved []string `json:"hdd_paths_preserved"`
|
||||
HDDPathsMissing []string `json:"hdd_paths_missing,omitempty"`
|
||||
HDDNote string `json:"hdd_note,omitempty"`
|
||||
// UserdataKept: as DeleteResponse.UserdataKept (decision 67, R-800).
|
||||
UserdataKept []string `json:"userdata_kept"`
|
||||
BackupPathsRemoved []string `json:"backup_paths_removed,omitempty"`
|
||||
BackupPathsRefused []string `json:"backup_paths_refused,omitempty"`
|
||||
// Verified says the teardown was CHECKED, not just requested (R-626/R-633). False means a
|
||||
@@ -149,10 +154,12 @@ func (m *Manager) hddPathForRemoval(op, name, composePath string, removeHDDData
|
||||
|
||||
// hddNoteFor composes the one-sentence HDDNote (see DeleteResponse). Only when the data was asked
|
||||
// for: a kept-data removal has nothing to explain about what was not found.
|
||||
func hddNoteFor(removeHDDData bool, mounts, missing []string) string {
|
||||
func hddNoteFor(removeHDDData bool, mounts, missing, userdataKept []string) string {
|
||||
switch {
|
||||
case !removeHDDData:
|
||||
return ""
|
||||
case len(mounts) == 0 && len(userdataKept) > 0:
|
||||
return "" // R-800: the page names the household's kept folders (userdata_kept) — "no data on a drive" would be false
|
||||
case len(mounts) == 0:
|
||||
return noteNoDriveData
|
||||
case len(missing) > 0:
|
||||
@@ -161,6 +168,36 @@ func hddNoteFor(removeHDDData bool, mounts, missing []string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// userdataKept lists the folders the app binds through ${USERDATA_PATH} that exist on its drive — the household's own
|
||||
// files (books, videos, photos it sees in the file browser). No remove deletes them (`09` §3 decision 67, R-800): the
|
||||
// removal reads ${HDD_PATH} binds only, and ProtectedHDDPaths guards the roots. This list exists so the dialog and the
|
||||
// result can SAY so, by name. USERDATA_PATH is <HDD_PATH>/userdata (withUserdataPath at deploy). Pinned by
|
||||
// TestRemoveStack_R800_UserdataKeptIsNamed.
|
||||
func (m *Manager) userdataKept(name, composePath string) []HDDPath {
|
||||
out := []HDDPath{}
|
||||
hddPath, declared := m.appHDDPath(name)
|
||||
if !declared {
|
||||
return out
|
||||
}
|
||||
for _, p := range ParseComposeUserdataMounts(composePath, appbackup.UserdataDir(hddPath)) {
|
||||
clean := filepath.Clean(p)
|
||||
info, err := os.Stat(clean)
|
||||
if err != nil || !info.IsDir() {
|
||||
continue // never created (or gone): nothing of the household's is there to keep
|
||||
}
|
||||
out = append(out, HDDPath{Path: clean, Exists: true, SizeBytes: getDirSizeBytes(clean), SizeHuman: getDirSizeHuman(clean)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func userdataKeptStrings(ps []HDDPath) []string {
|
||||
out := make([]string, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
out = append(out, fmt.Sprintf("%s (%s)", p.Path, p.SizeHuman))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BackupDataResponse holds information about backup data associated with a stack.
|
||||
type BackupDataResponse struct {
|
||||
Stack string `json:"stack"`
|
||||
@@ -173,6 +210,9 @@ type HDDDataResponse struct {
|
||||
Stack string `json:"stack"`
|
||||
HDDPaths []HDDPath `json:"hdd_paths"`
|
||||
HasHDDData bool `json:"has_hdd_data"`
|
||||
// UserdataKept (`09` §3 decision 67, R-800): the household's own folders the app binds through ${USERDATA_PATH}
|
||||
// that exist. A remove never deletes them, "with data" included; the dialog says so and names them. Always non-nil.
|
||||
UserdataKept []HDDPath `json:"userdata_kept"`
|
||||
// KeepDataOnly (v0.269.0, `09` §3 decision 27, R-666): the app is held and its page says support is
|
||||
// informed — the remove dialog offers only "remove the app, keep my data", and the API refuses the rest.
|
||||
KeepDataOnly bool `json:"keep_data_only,omitempty"`
|
||||
@@ -274,6 +314,7 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
|
||||
Deleted: name,
|
||||
HDDPathsRemoved: []string{},
|
||||
HDDPathsPreserved: []string{},
|
||||
UserdataKept: userdataKeptStrings(m.userdataKept(name, stack.ComposePath)),
|
||||
}
|
||||
|
||||
// Step 1: Parse compose file for HDD bind mounts
|
||||
@@ -346,7 +387,7 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
|
||||
resp.HDDPathsPreserved = append(resp.HDDPathsPreserved, fmt.Sprintf("%s (%s)", cleanPath, sizeHuman))
|
||||
}
|
||||
}
|
||||
resp.HDDNote = hddNoteFor(removeHDDData, hddMounts, resp.HDDPathsMissing)
|
||||
resp.HDDNote = hddNoteFor(removeHDDData, hddMounts, resp.HDDPathsMissing, resp.UserdataKept)
|
||||
|
||||
// Step 5: Remove stack directory
|
||||
if m.isDebug() {
|
||||
@@ -382,7 +423,8 @@ func (m *Manager) GetStackHDDData(name string) (*HDDDataResponse, error) {
|
||||
// R-442: the app's own recorded HDD_PATH, not the global config (which no box sets).
|
||||
hddPath, declared := m.appHDDPath(name)
|
||||
resp := &HDDDataResponse{
|
||||
Stack: name,
|
||||
Stack: name,
|
||||
UserdataKept: m.userdataKept(name, stack.ComposePath),
|
||||
}
|
||||
|
||||
if !declared {
|
||||
@@ -608,6 +650,7 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
Removed: name,
|
||||
HDDPathsRemoved: []string{},
|
||||
HDDPathsPreserved: []string{},
|
||||
UserdataKept: userdataKeptStrings(m.userdataKept(name, stack.ComposePath)),
|
||||
}
|
||||
|
||||
// Step 1: Parse compose file for HDD bind mounts
|
||||
@@ -699,7 +742,7 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
}
|
||||
}
|
||||
|
||||
resp.HDDNote = hddNoteFor(removeHDDData, hddMounts, resp.HDDPathsMissing)
|
||||
resp.HDDNote = hddNoteFor(removeHDDData, hddMounts, resp.HDDPathsMissing, resp.UserdataKept)
|
||||
|
||||
// Step 5: Handle backup data cleanup. Model A: backups/ sits directly under the app's felhom-data
|
||||
// namespace root. R-442: that root is resolved by the SAME rule as the data half and as the
|
||||
|
||||
Reference in New Issue
Block a user