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 }