detached 6af0a61

disk.go RawHistoryFolder

1package disk
2
3import (
4	"errors"
5	"fmt"
6)
7
8// Disk is an entity representing a physical disk with partitions
9// It manages the collection of partitions and validates partition layout
10type Disk struct {
11	device     string
12	sizeGB     int64
13	partitions []*Partition
14}
15
16// NewDisk creates a new Disk entity with validation
17func NewDisk(device string, sizeGB int64) (*Disk, error) {
18	// Validate device
19	if device == "" {
20		return nil, errors.New("disk device cannot be empty")
21	}
22
23	// Validate size
24	if sizeGB < 1 {
25		return nil, errors.New("disk size must be at least 1GB")
26	}
27
28	// Reasonable limit (max 100TB)
29	if sizeGB > 104857600 {
30		return nil, errors.New("disk size is unreasonably large")
31	}
32
33	return &Disk{
34		device:     device,
35		sizeGB:     sizeGB,
36		partitions: []*Partition{},
37	}, nil
38}
39
40// Device returns the device path
41func (d *Disk) Device() string {
42	return d.device
43}
44
45// SizeGB returns the disk size in gigabytes
46func (d *Disk) SizeGB() int64 {
47	return d.sizeGB
48}
49
50// SizeMB returns the disk size in megabytes
51func (d *Disk) SizeMB() int64 {
52	return d.sizeGB * 1024
53}
54
55// Partitions returns a copy of the partitions list
56func (d *Disk) Partitions() []*Partition {
57	partitions := make([]*Partition, len(d.partitions))
58	copy(partitions, d.partitions)
59	return partitions
60}
61
62// PartitionCount returns the number of partitions
63func (d *Disk) PartitionCount() int {
64	return len(d.partitions)
65}
66
67// AddPartition adds a partition to the disk with validation
68func (d *Disk) AddPartition(partition *Partition) error {
69	if partition == nil {
70		return errors.New("partition cannot be nil")
71	}
72
73	// Check partition device is on this disk
74	if !isPartitionOfDisk(d.device, partition.device) {
75		return fmt.Errorf("partition %s is not on disk %s", partition.device, d.device)
76	}
77
78	// Check for duplicate device
79	for _, p := range d.partitions {
80		if p.device == partition.device {
81			return fmt.Errorf("partition %s already exists", partition.device)
82		}
83	}
84
85	// Check for duplicate mount points (except swap which has no mount point)
86	if partition.mountPoint != "" {
87		for _, p := range d.partitions {
88			if p.mountPoint == partition.mountPoint {
89				return fmt.Errorf("mount point %s is already in use", partition.mountPoint)
90			}
91		}
92	}
93
94	// Validate total size doesn't exceed disk size
95	if totalPartitionSize := d.calculateTotalPartitionSize() + partition.SizeGB(); totalPartitionSize > d.sizeGB {
96		return fmt.Errorf("partition size would exceed disk capacity (total: %dGB, disk: %dGB)",
97			totalPartitionSize, d.sizeGB)
98	}
99
100	d.partitions = append(d.partitions, partition)
101	return nil
102}
103
104// FindPartitionByMountPoint finds a partition by its mount point
105func (d *Disk) FindPartitionByMountPoint(mountPoint string) *Partition {
106	for _, p := range d.partitions {
107		if p.mountPoint == mountPoint {
108			return p
109		}
110	}
111	return nil
112}
113
114// FindPartitionByDevice finds a partition by its device path
115func (d *Disk) FindPartitionByDevice(device string) *Partition {
116	for _, p := range d.partitions {
117		if p.device == device {
118			return p
119		}
120	}
121	return nil
122}
123
124// HasRootPartition returns true if disk has a root (/) partition
125func (d *Disk) HasRootPartition() bool {
126	return d.FindPartitionByMountPoint("/") != nil
127}
128
129// HasBootPartition returns true if disk has a boot partition
130func (d *Disk) HasBootPartition() bool {
131	return d.FindPartitionByMountPoint("/boot") != nil || d.FindPartitionByMountPoint("/boot/efi") != nil
132}
133
134// HasEFIPartition returns true if disk has an EFI partition
135func (d *Disk) HasEFIPartition() bool {
136	return d.FindPartitionByMountPoint("/boot/efi") != nil
137}
138
139// GetEFIPartition returns the EFI partition if it exists
140func (d *Disk) GetEFIPartition() *Partition {
141	return d.FindPartitionByMountPoint("/boot/efi")
142}
143
144// GetRootPartition returns the root partition if it exists
145func (d *Disk) GetRootPartition() *Partition {
146	return d.FindPartitionByMountPoint("/")
147}
148
149// GetBootPartition returns the boot partition if it exists
150func (d *Disk) GetBootPartition() *Partition {
151	if p := d.FindPartitionByMountPoint("/boot/efi"); p != nil {
152		return p
153	}
154	return d.FindPartitionByMountPoint("/boot")
155}
156
157// CalculateUsedSpace returns total space used by partitions in GB
158func (d *Disk) CalculateUsedSpace() int64 {
159	return d.calculateTotalPartitionSize()
160}
161
162// CalculateFreeSpace returns remaining space in GB
163func (d *Disk) CalculateFreeSpace() int64 {
164	return d.sizeGB - d.calculateTotalPartitionSize()
165}
166
167// ValidateLayout validates the partition layout according to business rules
168func (d *Disk) ValidateLayout() error {
169	// At minimum, must have root partition
170	if !d.HasRootPartition() {
171		return errors.New("disk must have a root (/) partition")
172	}
173
174	rootPartition := d.GetRootPartition()
175
176	// Root partition must be at least 20GB for comfortable installation
177	if rootPartition.SizeGB() < 20 {
178		return errors.New("root partition must be at least 20GB")
179	}
180
181	// For UEFI boot, must have EFI partition
182	// (This is a business rule - we assume UEFI for now)
183	if !d.HasEFIPartition() {
184		return errors.New("disk must have an EFI partition (/boot/efi) for UEFI boot")
185	}
186
187	// EFI partition must be FAT32
188	efiPartition := d.GetEFIPartition()
189	if efiPartition.Filesystem() != FilesystemFAT32 {
190		return errors.New("EFI partition must use FAT32 filesystem")
191	}
192
193	// EFI partition must be at least 256MB (typical 512MB)
194	if efiPartition.SizeGB() < 1 {
195		// Less than 1GB is ok for EFI if it's at least 256MB
196		if efiPartition.SizeMB() < 256 {
197			return errors.New("EFI partition must be at least 256MB")
198		}
199	}
200
201	// No duplicate mount points
202	mountPoints := make(map[string]bool)
203	for _, p := range d.partitions {
204		if p.mountPoint != "" {
205			if mountPoints[p.mountPoint] {
206				return fmt.Errorf("duplicate mount point: %s", p.mountPoint)
207			}
208			mountPoints[p.mountPoint] = true
209		}
210	}
211
212	return nil
213}
214
215// String returns human-readable representation
216func (d *Disk) String() string {
217	return fmt.Sprintf("Disk(device=%s, size=%dGB, partitions=%d)", d.device, d.sizeGB, len(d.partitions))
218}
219
220// Private helper methods
221
222func (d *Disk) calculateTotalPartitionSize() int64 {
223	var total int64
224	for _, p := range d.partitions {
225		total += p.SizeGB()
226	}
227	return total
228}
229
230// isPartitionOfDisk checks if a partition device belongs to a disk
231// e.g., /dev/sda1 belongs to /dev/sda, /dev/nvme0n1p1 belongs to /dev/nvme0n1
232func isPartitionOfDisk(diskDevice, partitionDevice string) bool {
233	if len(partitionDevice) <= len(diskDevice) {
234		return false
235	}
236
237	// Check prefix matches
238	if partitionDevice[:len(diskDevice)] != diskDevice {
239		return false
240	}
241
242	// Next character can be 'p' (for nvme: nvme0n1p1) or a digit (for sda: sda1)
243	nextChar := partitionDevice[len(diskDevice)]
244	return (nextChar >= '0' && nextChar <= '9') || nextChar == 'p'
245}

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