2064f32199
Bug 1 (sfdisk): Add wipefs before sfdisk; change partition type from ,,,L (unsupported GPT shorthand) to ,, (default Linux GUID); add --force --wipe always flags to handle existing partition tables. Bug 2 (mount): Replace fstab-lookup mount with explicit device path: mount -t ext4 -o defaults,noatime /host-dev/sdb1 /mnt/hdd_1 Container's /etc/fstab is Docker's auto-generated one, not the host's. Bug 3 (mount propagation): Change /mnt volume to long-form bind with propagation: rshared so mounts created inside container propagate to the host. Requires mount --make-rshared /mnt on host before restart. Safety: Use req.MountName (ASCII) for ext4 -L label (16-byte limit; UTF-8 display label stays in settings.json). Add findmnt verification after mount. Improve progress messages with command details. Smart partition: In storageInitAPIHandler, if disk already has exactly 1 empty partition (no filesystem), skip wipefs+sfdisk and format the existing partition directly. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
198 lines
6.9 KiB
Go
198 lines
6.9 KiB
Go
//go:build linux
|
|
|
|
package storage
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
|
|
mountPath := "/mnt/" + req.MountName
|
|
|
|
// --- 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)
|
|
}
|
|
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
|
|
send("partitioning", fmt.Sprintf("wipefs -a %s ...", HostDevicePath(req.DevicePath)), 12)
|
|
_ = exec.Command("wipefs", "-a", HostDevicePath(req.DevicePath)).Run()
|
|
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.
|
|
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 {
|
|
return "", fail("partitioning", "Partícionálás sikertelen: "+string(out), err)
|
|
}
|
|
|
|
_ = 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]
|
|
}
|
|
|
|
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 {
|
|
return "", fail("formatting", "Formázás sikertelen: "+mkfsOut.String(), err)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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 {
|
|
return "", fail("mounting", "UUID lekérése sikertelen", err)
|
|
}
|
|
uuid := strings.TrimSpace(string(uuidOut))
|
|
if uuid == "" {
|
|
return "", fail("mounting", "UUID üres a formázás után", fmt.Errorf("empty UUID"))
|
|
}
|
|
|
|
// Backup fstab (non-fatal)
|
|
_ = BackupFstab(FstabPath)
|
|
|
|
if err := AppendFstabEntry(FstabPath, uuid, mountPath, "ext4", "defaults,nofail,noatime"); err != nil {
|
|
return "", fail("mounting", "fstab bejegyzés hozzáadása sikertelen", err)
|
|
}
|
|
|
|
// Mount by device path explicitly — container's /etc/fstab != host fstab,
|
|
// so "mount /mnt/hdd_1" (fstab lookup) won't work.
|
|
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 {
|
|
return "", fail("mounting", "Csatlakoztatás sikertelen: "+string(out), err)
|
|
}
|
|
|
|
// Verify mount actually worked (don't just trust exit code)
|
|
verifyOut, verifyErr := exec.Command("findmnt", "-n", "-o", "SOURCE", "--target", mountPath).Output()
|
|
if verifyErr != nil || strings.TrimSpace(string(verifyOut)) == "" {
|
|
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))
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|