//go:build linux package storage import ( "bytes" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/util" ) // FormatAndMount formats a disk/partition and mounts it. // Progress updates are sent on the progress channel. // Returns the final mount path on success. func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, error) { send := func(step, msg string, pct int) { progress <- FormatProgress{Step: step, Message: msg, Percent: pct} } fail := func(step, msg string, err error) error { errStr := "" if err != nil { errStr = err.Error() } progress <- FormatProgress{Step: "error", Message: msg, Error: errStr, Percent: 0} return fmt.Errorf("%s: %w", msg, err) } dbg := func(format string, args ...interface{}) { if req.Logger != nil && req.Debug { req.Logger.Printf("[DEBUG] FormatAndMount: "+format, args...) } } mountPath := "/mnt/" + req.MountName dbg("starting: device=%s mountName=%s createPartition=%v", req.DevicePath, req.MountName, req.CreatePartition) // --- Step 1: Validate --- send("validating", "Eszköz ellenőrzése...", 5) if err := ValidateMountName(req.MountName); err != nil { return "", fail("validating", "Érvénytelen csatlakoztatási név", err) } // C6: Validate DevicePath to prevent path traversal from user-supplied input. if !strings.HasPrefix(req.DevicePath, "/dev/") { return "", fail("validating", "Érvénytelen eszközútvonal: /dev/-vel kell kezdődnie", fmt.Errorf("invalid device path: must start with /dev/")) } if strings.Contains(req.DevicePath, "..") { return "", fail("validating", "Érvénytelen eszközútvonal: nem tartalmazhat ..-t", fmt.Errorf("invalid device path: must not contain ..")) } if _, err := os.Stat(HostDevicePath(req.DevicePath)); err != nil { return "", fail("validating", "Az eszköz nem létezik: "+req.DevicePath, err) } isSystem, err := IsSystemDisk(req.DevicePath) if err != nil { return "", fail("validating", "Rendszermeghajtó ellenőrzése sikertelen", err) } if isSystem { return "", fail("validating", "Ez a rendszermeghajtó — nem formázható!", fmt.Errorf("device is system disk")) } mounted, err := IsDeviceMounted(req.DevicePath) if err != nil { return "", fail("validating", "Csatlakoztatási állapot ellenőrzése sikertelen", err) } if mounted { return "", fail("validating", "Az eszköz már csatlakoztatva van", fmt.Errorf("device already mounted")) } inUse, err := IsMountPathInUse(mountPath) if err != nil { return "", fail("validating", "Csatlakoztatási útvonal ellenőrzése sikertelen", err) } if inUse { return "", fail("validating", "A csatlakoztatási útvonal már használatban van: "+mountPath, fmt.Errorf("mount path in use")) } send("validating", "Ellenőrzés kész", 10) // --- Step 2: Partition (if requested) --- partDev := req.DevicePath if req.CreatePartition { // Wipe existing partition table and filesystem signatures first // H18: Log wipefs errors instead of silently discarding them. dbg("step wipefs: wipefs -a %s", HostDevicePath(req.DevicePath)) send("partitioning", fmt.Sprintf("wipefs -a %s ...", HostDevicePath(req.DevicePath)), 12) if err := exec.Command("wipefs", "-a", HostDevicePath(req.DevicePath)).Run(); err != nil { // Non-fatal: some systems don't have wipefs; continue anyway dbg("wipefs failed (non-fatal): %v", err) send("partitioning", fmt.Sprintf("[WARN] wipefs sikertelen %s: %v (folytatás)", req.DevicePath, err), 13) } else { dbg("wipefs completed successfully") } time.Sleep(500 * time.Millisecond) // Create GPT with single partition spanning whole disk. // ",," = start=default, size=default(fill disk), type=default(Linux filesystem GUID). // --force: overwrite even if device appears busy. // --wipe always: wipe filesystem signatures from newly created partitions. dbg("step sfdisk: sfdisk --force --wipe always %s", HostDevicePath(req.DevicePath)) send("partitioning", fmt.Sprintf("sfdisk --force --wipe always %s ...", HostDevicePath(req.DevicePath)), 15) sfdiskInput := "label: gpt\n,,\n" cmd := exec.Command("sfdisk", "--force", "--wipe", "always", HostDevicePath(req.DevicePath)) cmd.Stdin = strings.NewReader(sfdiskInput) if out, err := cmd.CombinedOutput(); err != nil { dbg("sfdisk failed: %s", util.TruncateStr(string(out), 500)) return "", fail("partitioning", "Partícionálás sikertelen: "+string(out), err) } else { dbg("sfdisk output: %s", util.TruncateStr(string(out), 500)) } _ = exec.Command("partprobe", HostDevicePath(req.DevicePath)).Run() time.Sleep(2 * time.Second) partDev = req.DevicePath + "1" if strings.Contains(req.DevicePath, "nvme") { partDev = req.DevicePath + "p1" } if _, err := os.Stat(HostDevicePath(partDev)); err != nil { return "", fail("partitioning", "Partíció nem található a létrehozás után: "+partDev, err) } send("partitioning", "Partíció létrehozva: "+partDev, 25) } // --- Step 3: Format --- // Use ASCII-safe mount name for ext4 filesystem label (16-byte limit). // The display label (req.Label) stays in settings.json for the UI. fsLabel := req.MountName if len(fsLabel) > 16 { fsLabel = fsLabel[:16] } dbg("step mkfs.ext4: mkfs.ext4 -L %s -F %s", fsLabel, HostDevicePath(partDev)) send("formatting", fmt.Sprintf("mkfs.ext4 -L %s -F %s ...", fsLabel, HostDevicePath(partDev)), 30) mkfsCmd := exec.Command("mkfs.ext4", "-L", fsLabel, "-F", HostDevicePath(partDev)) var mkfsOut bytes.Buffer mkfsCmd.Stdout = &mkfsOut mkfsCmd.Stderr = &mkfsOut if err := mkfsCmd.Run(); err != nil { dbg("mkfs.ext4 failed: %s", util.TruncateStr(mkfsOut.String(), 500)) return "", fail("formatting", "Formázás sikertelen: "+mkfsOut.String(), err) } dbg("mkfs.ext4 output: %s", util.TruncateStr(mkfsOut.String(), 500)) send("formatting", "Formázás kész", 60) // --- Step 4: Mount --- if err := os.MkdirAll(mountPath, 0755); err != nil { return "", fail("mounting", "Csatlakoztatási mappa nem hozható létre: "+mountPath, err) } dbg("step blkid: blkid -s UUID -o value %s", HostDevicePath(partDev)) send("mounting", fmt.Sprintf("UUID lekérése: blkid %s ...", HostDevicePath(partDev)), 65) uuidOut, err := exec.Command("blkid", "-s", "UUID", "-o", "value", HostDevicePath(partDev)).Output() if err != nil { dbg("blkid UUID failed: %v", err) return "", fail("mounting", "UUID lekérése sikertelen", err) } uuid := strings.TrimSpace(string(uuidOut)) dbg("blkid returned UUID=%q", uuid) if uuid == "" { return "", fail("mounting", "UUID üres a formázás után", fmt.Errorf("empty UUID")) } // Backup fstab (non-fatal) _ = BackupFstab(FstabPath) dbg("step fstab: appending UUID=%s mountPath=%s fstype=ext4", uuid, mountPath) if err := AppendFstabEntry(FstabPath, uuid, mountPath, "ext4", "defaults,nofail,noatime"); err != nil { dbg("fstab append failed: %v", err) return "", fail("mounting", "fstab bejegyzés hozzáadása sikertelen", err) } dbg("fstab entry added successfully") // Mount by device path explicitly — container's /etc/fstab != host fstab, // so "mount /mnt/hdd_1" (fstab lookup) won't work. dbg("step mount: mount -t ext4 -o defaults,noatime %s %s", HostDevicePath(partDev), mountPath) send("mounting", fmt.Sprintf("mount -t ext4 %s %s ...", HostDevicePath(partDev), mountPath), 70) if out, err := exec.Command("mount", "-t", "ext4", "-o", "defaults,noatime", HostDevicePath(partDev), mountPath).CombinedOutput(); err != nil { dbg("mount failed: %s", util.TruncateStr(string(out), 500)) // H19: Roll back fstab entry to prevent orphaned entry that hangs system on reboot. _ = RemoveFstabEntry(FstabPath, uuid) return "", fail("mounting", "Csatlakoztatás sikertelen: "+string(out), err) } // Verify mount actually worked (don't just trust exit code) dbg("step verify: findmnt -n -o SOURCE --target %s", mountPath) verifyOut, verifyErr := exec.Command("findmnt", "-n", "-o", "SOURCE", "--target", mountPath).Output() if verifyErr != nil || strings.TrimSpace(string(verifyOut)) == "" { dbg("mount verification failed: findmnt returned %q err=%v", string(verifyOut), verifyErr) // H19: Also roll back fstab if mount verify fails. _ = RemoveFstabEntry(FstabPath, uuid) return "", fail("mounting", "A csatlakoztatás nem ellenőrizhető: mount sikerült, de a meghajtó nem látható", fmt.Errorf("mount point %s not found after mount", mountPath)) } dbg("mount verified: findmnt source=%q", strings.TrimSpace(string(verifyOut))) send("mounting", "Csatlakoztatva: "+mountPath, 80) // --- Step 5: Permissions + subdirs --- send("permissions", "Mappák létrehozása és jogosultságok beállítása...", 85) _ = exec.Command("chown", "1000:1000", mountPath).Run() for _, subdir := range []string{"storage", "Dokumentumok"} { dir := filepath.Join(mountPath, subdir) if err := os.MkdirAll(dir, 0755); err == nil { _ = exec.Command("chown", "1000:1000", dir).Run() } } dbg("format and mount completed successfully: %s", mountPath) send("done", "Meghajtó sikeresen inicializálva: "+mountPath, 100) return mountPath, nil } // GetDeviceUUID returns the UUID of a block device/partition. func GetDeviceUUID(devicePath string) (string, error) { out, err := exec.Command("blkid", "-s", "UUID", "-o", "value", HostDevicePath(devicePath)).Output() if err != nil { return "", err } return strings.TrimSpace(string(out)), nil } // ReadFstab reads the current fstab content. func ReadFstab() (string, error) { data, err := os.ReadFile(FstabPath) if err != nil { data, err = os.ReadFile("/etc/fstab") if err != nil { return "", err } } return string(data), nil }