package drives import ( "bufio" "context" "fmt" "os/exec" "path/filepath" "strings" "time" "tuistream/internal/step" ) // MountOptions captures the user's choices for an Add-drive run. type MountOptions struct { Drive Drive // the chosen partition Label string // friendly name (becomes the directory under /media//) User string // target user (SUDO_USER); ACLs and ownership go to this user UserGroup string // primary group of User; "" → resolve via getent FormatAs FormatChoice // "" = keep existing fs; otherwise mkfs. with -F/-f // WipeWholeDisk: nuke the entire parent disk (zap GPT, wipefs every // signature, partprobe) before mkfs, and use the whole-disk path for // mkfs and the fstab UUID lookup. Only meaningful when FormatAs is set — // "keep existing" is a no-op for wipe. Destroys any OTHER partitions on // the parent disk; the confirm screen lists them so this is explicit. WipeWholeDisk bool // SeedStarterFolders creates the Jellyfin library starter folders // (StarterFolders) under /JellyfinMedia/. The TUI leaves this off // when keeping an existing filesystem that already holds the user's own // content, so we don't litter their layout with empty library dirs. SeedStarterFolders bool // RecursiveACL grants the jellyfin user read access over the ENTIRE existing // tree (setfacl -R), not just the mount root + future files. Set when we're // keeping a drive that already holds media: a non-recursive grant would // leave pre-existing files unreadable to Jellyfin if they were copied with // tight permissions. Harmless (read-only grant) but can take a moment on a // very large library, so it's only switched on when there's content to fix. RecursiveACL bool } // EffectiveFSType returns the filesystem the drive will have AFTER the plan // runs — i.e. the formatted-to choice if we're formatting, else the // existing filesystem. func (o MountOptions) EffectiveFSType() string { if o.FormatAs != FormatKeep { return string(o.FormatAs) } return o.Drive.FSType } // EffectiveTargetDevice returns the block-device path mkfs/blkid/mount will // actually operate on. If WipeWholeDisk is set AND a parent disk was // resolved, that's the whole disk (/dev/sda); for a whole-disk Drive // (Type=="disk") it's already the disk; otherwise it's the chosen partition. func (o MountOptions) EffectiveTargetDevice() string { if o.WipeWholeDisk && o.Drive.ParentDisk != "" { return "/dev/" + o.Drive.ParentDisk } return o.Drive.Path } // WipeTarget returns the path of the whole-disk wipe step's victim, or "" if // no wipe step should run. We wipe whenever the user chose a destructive // format AND either explicitly opted into wipe-whole-disk or picked a // whole-disk Drive (where wiping the entire device is the only sensible // thing). func (o MountOptions) WipeTarget() string { if o.FormatAs == FormatKeep { return "" } if o.Drive.Type == "disk" { // Whole-disk Drive — wipe self. return o.Drive.Path } if o.WipeWholeDisk && o.Drive.ParentDisk != "" { return "/dev/" + o.Drive.ParentDisk } return "" } // MountPlan builds the ordered list of commands needed to attach `opts.Drive` // as a media drive at /media//