main 6af0a61

partition_handler.go RawHistoryFolder

1package handlers
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/bnema/archup/internal/application/commands"
8	"github.com/bnema/archup/internal/application/dto"
9	"github.com/bnema/archup/internal/domain/disk"
10	"github.com/bnema/archup/internal/domain/ports"
11)
12
13// PartitionHandler handles disk partitioning, formatting, and mounting
14type PartitionHandler struct {
15	cmdExec ports.CommandExecutor
16	logger  ports.Logger
17}
18
19// NewPartitionHandler creates a new partition handler
20func NewPartitionHandler(cmdExec ports.CommandExecutor, logger ports.Logger) *PartitionHandler {
21	return &PartitionHandler{
22		cmdExec: cmdExec,
23		logger:  logger,
24	}
25}
26
27// Handle executes the complete disk partitioning workflow
28func (h *PartitionHandler) Handle(ctx context.Context, cmd commands.PartitionDiskCommand) (*dto.PartitionResult, error) {
29	h.logger.Info("Starting disk partitioning", "disk", cmd.TargetDisk)
30
31	result := &dto.PartitionResult{
32		TargetDisk:  cmd.TargetDisk,
33		Success:     false,
34		Partitions:  []*dto.PartitionInfo{},
35		ErrorDetail: "",
36	}
37
38	// Step 1: Wipe disk if requested
39	if cmd.WipeDisks {
40		h.logger.Info("Wiping disk", "disk", cmd.TargetDisk)
41		if err := h.wipeDisks(ctx, cmd.TargetDisk); err != nil {
42			h.logger.Error("Failed to wipe disk", "error", err)
43			result.ErrorDetail = fmt.Sprintf("Failed to wipe disk: %v", err)
44			return result, err
45		}
46	}
47
48	// Step 2: Create GPT partitions (EFI + ROOT)
49	h.logger.Info("Creating GPT partition table")
50	efiPartition, rootPartition, err := h.createGPTPartitions(ctx, cmd.TargetDisk)
51	if err != nil {
52		h.logger.Error("Failed to create partitions", "error", err)
53		result.ErrorDetail = fmt.Sprintf("Failed to create partitions: %v", err)
54		return result, err
55	}
56
57	result.EFIPartition = efiPartition
58	result.RootPartition = rootPartition
59
60	// Step 3: Format EFI partition as FAT32
61	h.logger.Info("Formatting EFI partition", "partition", efiPartition)
62	if err := h.formatEFIPartition(ctx, efiPartition); err != nil {
63		h.logger.Error("Failed to format EFI partition", "error", err)
64		result.ErrorDetail = fmt.Sprintf("Failed to format EFI partition: %v", err)
65		return result, err
66	}
67
68	// Step 4: Handle root partition (with optional encryption)
69	var rootDevice string
70	if cmd.EncryptionType != disk.EncryptionTypeNone {
71		h.logger.Info("Setting up LUKS encryption")
72		cryptDevice, err := h.setupLUKSEncryption(ctx, rootPartition, cmd.EncryptionPassword)
73		if err != nil {
74			h.logger.Error("Failed to setup LUKS encryption", "error", err)
75			result.ErrorDetail = fmt.Sprintf("Failed to setup encryption: %v", err)
76			return result, err
77		}
78		result.CryptDevice = cryptDevice
79		rootDevice = cryptDevice
80	} else {
81		rootDevice = rootPartition
82	}
83
84	// Step 5: Format root partition as Btrfs
85	h.logger.Info("Formatting root partition as Btrfs", "device", rootDevice)
86	if err := h.formatRootPartition(ctx, rootDevice); err != nil {
87		h.logger.Error("Failed to format root partition", "error", err)
88		result.ErrorDetail = fmt.Sprintf("Failed to format root partition: %v", err)
89		return result, err
90	}
91
92	// Step 6: Create Btrfs subvolumes
93	h.logger.Info("Creating Btrfs subvolumes")
94	layout, err := disk.NewStandardBtrfsLayout()
95	if err != nil {
96		h.logger.Error("Failed to create Btrfs layout", "error", err)
97		result.ErrorDetail = fmt.Sprintf("Failed to create Btrfs layout: %v", err)
98		return result, err
99	}
100
101	subvolumes, err := h.createBtrfsSubvolumes(ctx, rootDevice, layout)
102	if err != nil {
103		h.logger.Error("Failed to create Btrfs subvolumes", "error", err)
104		result.ErrorDetail = fmt.Sprintf("Failed to create Btrfs subvolumes: %v", err)
105		return result, err
106	}
107	result.Subvolumes = subvolumes
108
109	// Step 7: Mount filesystems
110	h.logger.Info("Mounting filesystems")
111	mounts, err := h.mountFilesystems(ctx, efiPartition, rootDevice, layout)
112	if err != nil {
113		h.logger.Error("Failed to mount filesystems", "error", err)
114		result.ErrorDetail = fmt.Sprintf("Failed to mount filesystems: %v", err)
115		return result, err
116	}
117	result.MountedAt = mounts
118
119	// Build partition info for result
120	result.Partitions = []*dto.PartitionInfo{
121		{
122			Device:     efiPartition,
123			SizeGB:     cmd.BootSizeGB,
124			Filesystem: "FAT32",
125			MountPoint: "/boot",
126			Encrypted:  false,
127		},
128		{
129			Device:     rootPartition,
130			SizeGB:     cmd.RootSizeGB,
131			Filesystem: "Btrfs",
132			MountPoint: "/",
133			Encrypted:  cmd.EncryptionType != disk.EncryptionTypeNone,
134		},
135	}
136
137	result.Success = true
138	h.logger.Info("Disk partitioning completed successfully")
139	return result, nil
140}
141
142// wipeDisks wipes filesystem signatures from disk
143func (h *PartitionHandler) wipeDisks(ctx context.Context, diskPath string) error {
144	h.logger.Info("Wiping filesystem signatures", "disk", diskPath)
145
146	// Validate disk path
147	if err := disk.ValidateDiskPath(diskPath); err != nil {
148		return fmt.Errorf("invalid disk path: %w", err)
149	}
150
151	// wipefs -af removes all signatures
152	if _, err := h.cmdExec.Execute(ctx, "wipefs", "-af", diskPath); err != nil {
153		return fmt.Errorf("wipefs failed: %w", err)
154	}
155
156	// sgdisk --zap-all removes GPT and MBR
157	if _, err := h.cmdExec.Execute(ctx, "sgdisk", "--zap-all", diskPath); err != nil {
158		return fmt.Errorf("sgdisk zap failed: %w", err)
159	}
160
161	h.logger.Info("Disk wiped successfully", "disk", diskPath)
162	return nil
163}
164
165// createGPTPartitions creates GPT partition table with EFI (4GB) and ROOT partitions
166func (h *PartitionHandler) createGPTPartitions(ctx context.Context, diskPath string) (string, string, error) {
167	h.logger.Info("Creating GPT partition table", "disk", diskPath)
168
169	// Validate disk path
170	if err := disk.ValidateDiskPath(diskPath); err != nil {
171		return "", "", fmt.Errorf("invalid disk path: %w", err)
172	}
173
174	// Create partitions using sgdisk
175	// Partition 1: 4GB EFI (type ef00) - recommended for limine-snapper-sync
176	// Partition 2: Remaining space ROOT (type 8300)
177	if _, err := h.cmdExec.Execute(ctx, "sgdisk",
178		"--clear",
179		"--new=1:0:+4G", "--typecode=1:ef00", "--change-name=1:EFI",
180		"--new=2:0:0", "--typecode=2:8300", "--change-name=2:ROOT",
181		diskPath); err != nil {
182		return "", "", fmt.Errorf("sgdisk partition creation failed: %w", err)
183	}
184
185	// Inform kernel of partition table changes
186	if _, err := h.cmdExec.Execute(ctx, "partprobe", diskPath); err != nil {
187		h.logger.Warn("partprobe failed (not critical)", "error", err)
188	}
189
190	// Determine partition paths based on disk type
191	efiPartition, err := disk.DeterminePartitionPath(diskPath, 1)
192	if err != nil {
193		return "", "", fmt.Errorf("failed to determine EFI partition path: %w", err)
194	}
195
196	rootPartition, err := disk.DeterminePartitionPath(diskPath, 2)
197	if err != nil {
198		return "", "", fmt.Errorf("failed to determine ROOT partition path: %w", err)
199	}
200
201	h.logger.Info("Partitions created", "efi", efiPartition, "root", rootPartition)
202	return efiPartition, rootPartition, nil
203}
204
205// formatEFIPartition formats EFI partition as FAT32
206func (h *PartitionHandler) formatEFIPartition(ctx context.Context, partitionPath string) error {
207	h.logger.Info("Formatting EFI partition", "partition", partitionPath)
208
209	// Validate partition path
210	if err := disk.ValidatePartitionPath(partitionPath); err != nil {
211		return fmt.Errorf("invalid partition path: %w", err)
212	}
213
214	// Wipe partition first
215	if _, err := h.cmdExec.Execute(ctx, "wipefs", "-af", partitionPath); err != nil {
216		return fmt.Errorf("wipefs EFI partition failed: %w", err)
217	}
218
219	// Format as FAT32 with label "EFI"
220	if _, err := h.cmdExec.Execute(ctx, "mkfs.fat", "-F32", "-n", "EFI", partitionPath); err != nil {
221		return fmt.Errorf("mkfs.fat failed: %w", err)
222	}
223
224	h.logger.Info("EFI partition formatted successfully")
225	return nil
226}
227
228// setupLUKSEncryption sets up LUKS2 encryption on root partition
229func (h *PartitionHandler) setupLUKSEncryption(ctx context.Context, partitionPath, password string) (string, error) {
230	h.logger.Info("Setting up LUKS encryption", "partition", partitionPath)
231
232	// Validate partition path
233	if err := disk.ValidatePartitionPath(partitionPath); err != nil {
234		return "", fmt.Errorf("invalid partition path: %w", err)
235	}
236
237	// Validate encryption password
238	encConfig, err := disk.NewEncryptionConfig(disk.EncryptionTypeLUKS, password)
239	if err != nil {
240		return "", fmt.Errorf("invalid encryption configuration: %w", err)
241	}
242
243	if !encConfig.IsEncrypted() {
244		return "", fmt.Errorf("encryption config invalid")
245	}
246
247	// Wipe partition first
248	if _, err := h.cmdExec.Execute(ctx, "wipefs", "-af", partitionPath); err != nil {
249		return "", fmt.Errorf("wipefs root partition failed: %w", err)
250	}
251
252	// Feed the passphrase over stdin so shell metacharacters are treated as data.
253	if _, err := h.cmdExec.ExecuteWithStdin(ctx, password, "cryptsetup",
254		"luksFormat",
255		"--type", "luks2",
256		"--batch-mode",
257		"--pbkdf", "argon2id",
258		"--iter-time", "2000",
259		"--label", "ARCHUP_LUKS",
260		"--key-file=-",
261		partitionPath,
262	); err != nil {
263		return "", fmt.Errorf("luksFormat failed: %w", err)
264	}
265
266	// Open LUKS container
267	cryptDevice := "/dev/mapper/cryptroot"
268	if _, err := h.cmdExec.ExecuteWithStdin(ctx, password, "cryptsetup",
269		"open",
270		"--key-file=-",
271		partitionPath,
272		"cryptroot",
273	); err != nil {
274		return "", fmt.Errorf("cryptsetup open failed: %w", err)
275	}
276
277	h.logger.Info("LUKS encryption setup successfully", "cryptDevice", cryptDevice)
278	return cryptDevice, nil
279}
280
281// formatRootPartition formats root partition (or encrypted device) as Btrfs
282func (h *PartitionHandler) formatRootPartition(ctx context.Context, devicePath string) error {
283	h.logger.Info("Formatting root partition as Btrfs", "device", devicePath)
284
285	// Format as Btrfs with label "ROOT"
286	if _, err := h.cmdExec.Execute(ctx, "mkfs.btrfs", "-f", "-L", "ROOT", devicePath); err != nil {
287		return fmt.Errorf("mkfs.btrfs failed: %w", err)
288	}
289
290	h.logger.Info("Root partition formatted successfully")
291	return nil
292}
293
294// createBtrfsSubvolumes creates Btrfs subvolumes according to layout
295func (h *PartitionHandler) createBtrfsSubvolumes(ctx context.Context, devicePath string, layout *disk.BtrfsLayout) ([]string, error) {
296	h.logger.Info("Creating Btrfs subvolumes", "device", devicePath)
297
298	// Validate layout
299	if err := layout.ValidateLayout(); err != nil {
300		return nil, fmt.Errorf("invalid Btrfs layout: %w", err)
301	}
302
303	// Mount temporarily to /mnt
304	if _, err := h.cmdExec.Execute(ctx, "mount", devicePath, "/mnt"); err != nil {
305		return nil, fmt.Errorf("temporary mount failed: %w", err)
306	}
307
308	// Ensure unmount on exit
309	defer func() {
310		if _, err := h.cmdExec.Execute(ctx, "umount", "/mnt"); err != nil {
311			h.logger.Warn("Failed to unmount temporary mount", "error", err)
312		}
313	}()
314
315	subvolumes := []string{}
316
317	// Create each subvolume
318	for _, sv := range layout.Subvolumes() {
319		subvolPath := "/mnt/" + sv.Name()
320		h.logger.Info("Creating subvolume", "name", sv.Name())
321
322		if _, err := h.cmdExec.Execute(ctx, "btrfs", "subvolume", "create", subvolPath); err != nil {
323			return nil, fmt.Errorf("failed to create subvolume %s: %w", sv.Name(), err)
324		}
325
326		subvolumes = append(subvolumes, sv.Name())
327	}
328
329	h.logger.Info("Btrfs subvolumes created successfully", "count", len(subvolumes))
330	return subvolumes, nil
331}
332
333// mountFilesystems mounts all filesystems to /mnt with proper options
334func (h *PartitionHandler) mountFilesystems(ctx context.Context, efiPartition, rootDevice string, layout *disk.BtrfsLayout) ([]string, error) {
335	h.logger.Info("Mounting filesystems")
336
337	mounts := []string{}
338
339	// Get root subvolume
340	rootSubvolume := layout.GetRootSubvolume()
341	if rootSubvolume == nil {
342		return nil, fmt.Errorf("no root subvolume found in layout")
343	}
344
345	// Mount @ subvolume as root with Btrfs options
346	rootMountOpts, err := disk.NewBtrfsMountOptions(rootSubvolume.Name())
347	if err != nil {
348		return nil, fmt.Errorf("failed to create mount options: %w", err)
349	}
350
351	if _, err := h.cmdExec.Execute(ctx, "mount", "-o", rootMountOpts.ToString(), rootDevice, "/mnt"); err != nil {
352		return nil, fmt.Errorf("failed to mount @ subvolume: %w", err)
353	}
354	mounts = append(mounts, "/mnt")
355
356	// Mount @home subvolume if it exists
357	homeSubvolume := layout.FindSubvolumeByName("@home")
358	if homeSubvolume != nil {
359		// Create /mnt/home directory
360		if _, err := h.cmdExec.Execute(ctx, "mkdir", "-p", "/mnt/home"); err != nil {
361			return nil, fmt.Errorf("failed to create /mnt/home: %w", err)
362		}
363
364		homeMountOpts, err := disk.NewBtrfsMountOptions(homeSubvolume.Name())
365		if err != nil {
366			return nil, fmt.Errorf("failed to create home mount options: %w", err)
367		}
368
369		if _, err := h.cmdExec.Execute(ctx, "mount", "-o", homeMountOpts.ToString(), rootDevice, "/mnt/home"); err != nil {
370			return nil, fmt.Errorf("failed to mount @home subvolume: %w", err)
371		}
372		mounts = append(mounts, "/mnt/home")
373	}
374
375	// Mount EFI partition to /mnt/boot
376	if _, err := h.cmdExec.Execute(ctx, "mkdir", "-p", "/mnt/boot"); err != nil {
377		return nil, fmt.Errorf("failed to create /mnt/boot: %w", err)
378	}
379
380	if _, err := h.cmdExec.Execute(ctx, "mount", efiPartition, "/mnt/boot"); err != nil {
381		return nil, fmt.Errorf("failed to mount EFI partition: %w", err)
382	}
383	mounts = append(mounts, "/mnt/boot")
384
385	h.logger.Info("All filesystems mounted successfully", "mounts", mounts)
386	return mounts, nil
387}
388
389// Rollback unmounts filesystems and closes LUKS device
390func (h *PartitionHandler) Rollback(ctx context.Context, result *dto.PartitionResult) error {
391	h.logger.Warn("Rolling back partitioning changes")
392
393	// Unmount in reverse order
394	for i := len(result.MountedAt) - 1; i >= 0; i-- {
395		mountPoint := result.MountedAt[i]
396		h.logger.Info("Unmounting", "mount", mountPoint)
397		if _, err := h.cmdExec.Execute(ctx, "umount", mountPoint); err != nil {
398			h.logger.Warn("Failed to unmount", "mount", mountPoint, "error", err)
399		}
400	}
401
402	// Close LUKS device if it exists
403	if result.CryptDevice != "" {
404		h.logger.Info("Closing LUKS device", "device", result.CryptDevice)
405		if _, err := h.cmdExec.Execute(ctx, "cryptsetup", "close", "cryptroot"); err != nil {
406			h.logger.Warn("Failed to close LUKS device", "error", err)
407		}
408	}
409
410	h.logger.Info("Rollback completed")
411	return nil
412}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close