a43d0423d5
gates / gates (push) Successful in 46s
felhom.eu architecture/16 §4-§6 (build B). MinAgent 0.157.0 — the internal place and the partitions appear only when agent >= 0.158.0 reports them. Tests red-proved; four parity fixtures refreshed (only the new lines). Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
109 lines
4.3 KiB
Go
109 lines
4.3 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"path"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// The internal data place („Belső tárhely", v0.318.0) — felhom.eu architecture/16-storage-physical-identity.md §5.
|
|
//
|
|
// On a one-disk box the installer (or an operator's signed job) makes a second volume on the system disk, and the
|
|
// agent (≥ 0.158.0) mounts it like a drive and reports it in GET /disks with kind „internal". The controller then,
|
|
// with no household press:
|
|
// 1. asks the agent to bind it into this guest (POST /disks/guest-attach — scoped to this guest's own token), and
|
|
// 2. on the next drive-gate tick, once the bind is live, registers it as a drive of kind internal — the DEFAULT
|
|
// drive when no other drive is default — so media and data-folder apps install with no extra step and no
|
|
// sentence (R-1068; the D4 refusal stays for a box with no drive at all).
|
|
//
|
|
// It is never offered for the drive lifecycle that would remove or format it (eject, decommission, wipe —
|
|
// refuseInternalLifecycle); the agent refuses those too (its device is on the system disk). Moving its data to
|
|
// another drive stays allowed. Its physical disk is the system disk, so build A never counts a copy between it and
|
|
// sys_drive as a second copy. Pinned by internal_place_test.go.
|
|
|
|
// internalPlaceAgent is the one agent call this needs (a seam for tests).
|
|
type internalPlaceAgent interface {
|
|
GuestAttach(ctx context.Context, where string) error
|
|
}
|
|
|
|
// ensureInternalPlace runs on every drive-gate tick (ReconcileDriveGates) with the agent's current /disks rows.
|
|
func (s *Server) ensureInternalPlace(ctx context.Context, agent internalPlaceAgent, disks []agentapi.DiskInfo) {
|
|
if s.settings == nil {
|
|
return
|
|
}
|
|
for _, d := range disks {
|
|
if d.Kind != agentapi.PlaceKindInternal || d.MountPath == "" || d.GuestPath == "" {
|
|
continue
|
|
}
|
|
gp := path.Clean(d.GuestPath)
|
|
if !d.BoundUnderParent {
|
|
// Bind it now; register on a later tick, once the bind is live (the skeleton is made in-guest).
|
|
if err := agent.GuestAttach(ctx, d.MountPath); err != nil {
|
|
s.logger.Printf("[WARN] [internal-place] guest bind of %s failed: %v", d.MountPath, err)
|
|
return
|
|
}
|
|
s.logger.Printf("[INFO] [internal-place] bound %s into the guest at %s", d.MountPath, gp)
|
|
return
|
|
}
|
|
for _, sp := range s.settings.GetStoragePaths() {
|
|
if path.Clean(sp.Path) == gp {
|
|
return // registered already (by us, or by the household on an older controller)
|
|
}
|
|
}
|
|
if err := s.registerInternalPlace(gp); err != nil {
|
|
s.logger.Printf("[WARN] [internal-place] register %s failed: %v", gp, err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// registerInternalPlace records the internal place as a drive of kind internal — the default drive unless the
|
|
// household already has one.
|
|
func (s *Server) registerInternalPlace(where string) error {
|
|
setDefault := true
|
|
for _, sp := range s.settings.GetStoragePaths() {
|
|
if sp.IsDefault && !sp.IsNetwork() && !sp.Decommissioned {
|
|
setDefault = false
|
|
}
|
|
}
|
|
if err := s.ensureUserdataSkeleton(where); err != nil {
|
|
s.logger.Printf("[WARN] [internal-place] userdata skeleton on %s: %v", where, err)
|
|
}
|
|
sp := settings.StoragePath{
|
|
Path: where,
|
|
Label: s.msgLang(s.settings.GetLanguage(), "storage.belso_tarhely"),
|
|
IsDefault: setDefault,
|
|
Schedulable: true,
|
|
AddedAt: time.Now().UTC().Format(time.RFC3339),
|
|
Kind: settings.StorageKindInternal,
|
|
}
|
|
if err := s.settings.AddStoragePath(sp); err != nil {
|
|
return err
|
|
}
|
|
s.logger.Printf("[INFO] [internal-place] registered %s (default=%v) — media and data-folder apps can use it", where, setDefault)
|
|
go s.SyncFileBrowserMounts()
|
|
s.mediaDriveEvent(where, "added")
|
|
return nil
|
|
}
|
|
|
|
// refuseInternalLifecycle blocks eject, decommission and wipe of the internal place (it is part of the system disk).
|
|
// Returns true (and writes the refusal) when the path is the internal place; the caller must then return.
|
|
func (s *Server) refuseInternalLifecycle(w http.ResponseWriter, r *http.Request, where string) bool {
|
|
if s.settings == nil {
|
|
return false
|
|
}
|
|
p := path.Clean(where)
|
|
for _, sp := range s.settings.GetStoragePaths() {
|
|
if sp.IsInternal() && path.Clean(sp.Path) == p {
|
|
writeDiskJSON(w, http.StatusBadRequest, false, s.msg(r, "err.web.belso_tarhely_eletciklus"), nil)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|