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}