package platform import ( "fmt" "os" "os/exec" "path/filepath" "sort" "strings" ) var exportExecCommand = exec.Command func formatMountTargetError(target RemovableTarget, raw string, err error) error { msg := strings.TrimSpace(raw) fstype := strings.ToLower(strings.TrimSpace(target.FSType)) if fstype == "exfat" && strings.Contains(strings.ToLower(msg), "unknown filesystem type 'exfat'") { return fmt.Errorf("mount %s: exFAT support is missing in this ISO build: %w", target.Device, err) } if msg == "" { return err } return fmt.Errorf("%s: %w", msg, err) } func removableTargetReadOnly(fields map[string]string) bool { if fields["RO"] == "1" { return true } switch strings.ToLower(strings.TrimSpace(fields["FSTYPE"])) { case "iso9660", "squashfs": return true default: return false } } func ensureWritableMountpoint(mountpoint string) error { probe, err := os.CreateTemp(mountpoint, ".bee-write-test-*") if err != nil { return fmt.Errorf("target filesystem is not writable: %w", err) } name := probe.Name() if closeErr := probe.Close(); closeErr != nil { _ = os.Remove(name) return closeErr } if err := os.Remove(name); err != nil { return err } return nil } func (s *System) ListRemovableTargets() ([]RemovableTarget, error) { raw, err := exportExecCommand("lsblk", "-P", "-o", "NAME,TYPE,PKNAME,RM,RO,FSTYPE,MOUNTPOINT,SIZE,LABEL,MODEL").Output() if err != nil { return nil, err } var out []RemovableTarget for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") { if strings.TrimSpace(line) == "" { continue } fields := parseLSBLKPairs(line) deviceType := fields["TYPE"] if deviceType == "rom" || deviceType == "loop" { continue } removable := fields["RM"] == "1" if !removable { if parent := fields["PKNAME"]; parent != "" { if data, err := os.ReadFile(filepath.Join("/sys/class/block", parent, "removable")); err == nil { removable = strings.TrimSpace(string(data)) == "1" } } } if !removable || fields["FSTYPE"] == "" || removableTargetReadOnly(fields) { continue } out = append(out, RemovableTarget{ Device: "/dev/" + fields["NAME"], FSType: fields["FSTYPE"], Size: fields["SIZE"], Label: fields["LABEL"], Model: fields["MODEL"], Mountpoint: fields["MOUNTPOINT"], }) } sort.Slice(out, func(i, j int) bool { return out[i].Device < out[j].Device }) return out, nil } func (s *System) ExportFileToTarget(src string, target RemovableTarget) (dst string, retErr error) { if src == "" || target.Device == "" { return "", fmt.Errorf("source and target are required") } if _, err := os.Stat(src); err != nil { return "", err } mountpoint := strings.TrimSpace(target.Mountpoint) mountedHere := false mounted := mountpoint != "" if mountpoint == "" { mountpoint = filepath.Join("/tmp", "bee-export-"+filepath.Base(target.Device)) if err := os.MkdirAll(mountpoint, 0755); err != nil { return "", err } if raw, err := exportExecCommand("mount", target.Device, mountpoint).CombinedOutput(); err != nil { _ = os.Remove(mountpoint) return "", formatMountTargetError(target, string(raw), err) } mountedHere = true mounted = true } defer func() { if !mounted { return } _ = exportExecCommand("sync").Run() if raw, err := exportExecCommand("umount", mountpoint).CombinedOutput(); err != nil && retErr == nil { msg := strings.TrimSpace(string(raw)) if msg == "" { retErr = err } else { retErr = fmt.Errorf("%s: %w", msg, err) } } if mountedHere { _ = os.Remove(mountpoint) } }() if err := ensureWritableMountpoint(mountpoint); err != nil { return "", err } filename := filepath.Base(src) dst = filepath.Join(mountpoint, filename) data, err := os.ReadFile(src) if err != nil { return "", err } if err := os.WriteFile(dst, data, 0644); err != nil { return "", err } return dst, nil }