This commit is contained in:
2025-02-16 06:44:43 +03:00
parent 299f6dea06
commit 01db4540b1
14 changed files with 788 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import os
import time
import freeflowuniverse.herolib.servers.imap
// Start the IMAP server on port 143
imap.start() or {
println("error in imap server")
eprint(err)
exit(1)
}

113
lib/servers/imap/README.md Normal file
View File

@@ -0,0 +1,113 @@
# IMAP Server
A simple IMAP server implementation in V that supports basic mailbox operations.
## Features
- In-memory IMAP server implementation
- Support for multiple mailboxes
- Basic IMAP commands: LOGIN, SELECT, FETCH, STORE, LOGOUT
- Message flags support (e.g. \Seen, \Flagged)
- Concurrent client handling
## Usage
The server can be started with a simple function call:
```v
import freeflowuniverse.herolib.servers.imap
fn main() {
// Start the IMAP server on port 143
imap.start() or { panic(err) }
}
```
Save this to `example.v` and run with:
```bash
v run example.v
```
The server will start listening on port 143 (default IMAP port) and initialize with an example INBOX containing two messages.
## Testing with an IMAP Client
You can test the server using any IMAP client. Here's an example using the `curl` command:
```bash
# Connect and login (any username/password is accepted)
curl "imap://localhost/" -u "user:pass" --ssl-reqd
# List messages in INBOX
curl "imap://localhost/INBOX" -u "user:pass" --ssl-reqd
```
## Implementation Details
The server consists of three main components:
1. **Model** (`model.v`): Defines the core data structures
- `Message`: Represents an email message with ID, subject, body and flags
- `Mailbox`: Contains a collection of messages
- `IMAPServer`: Holds the mailboxes map
2. **Server** (`server.v`): Handles the IMAP protocol implementation
- TCP connection handling
- IMAP command processing
- Concurrent client support
3. **Factory** (`factory.v`): Provides easy server initialization
- `start()` function to create and run the server
- Initializes example INBOX with sample messages
## Supported Commands
- `CAPABILITY`: List server capabilities
- `LOGIN`: Authenticate (accepts any credentials)
- `SELECT`: Select a mailbox
- `FETCH`: Retrieve message data
- `STORE`: Update message flags
- `LOGOUT`: End the session
## Example Session
```
C: A001 CAPABILITY
S: * CAPABILITY IMAP4rev1 AUTH=PLAIN
S: A001 OK CAPABILITY completed
C: A002 LOGIN user pass
S: A002 OK LOGIN completed
C: A003 SELECT INBOX
S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
S: * 2 EXISTS
S: A003 OK SELECT completed
C: A004 FETCH 1:* BODY[TEXT]
S: * 1 FETCH (FLAGS (\Seen) BODY[TEXT] "Welcome to the IMAP server!")
S: * 2 FETCH (FLAGS () BODY[TEXT] "This is an update.")
S: A004 OK FETCH completed
C: A005 STORE 2 +FLAGS (\Seen)
S: A005 OK STORE completed
C: A006 CAPABILITY
S: * CAPABILITY IMAP4rev1 AUTH=PLAIN
S: A006 OK CAPABILITY completed
C: A007 LOGOUT
S: * BYE IMAP4rev1 Server logging out
S: A007 OK LOGOUT completed
```
## Notes
- The server runs on port 143, which typically requires root privileges. Make sure you have the necessary permissions.
- This is a basic implementation for demonstration purposes. For production use, consider adding:
- Proper authentication
- Persistent storage
- Full IMAP command support
- TLS encryption
- Message parsing and MIME support

View File

@@ -0,0 +1,9 @@
module imap
import net
// handle_capability processes the CAPABILITY command
pub fn handle_capability(mut conn net.TcpConn, tag string) ! {
conn.write('* CAPABILITY IMAP4rev1 AUTH=PLAIN STARTTLS LOGIN\r\n'.bytes())!
conn.write('${tag} OK Completed\r\n'.bytes())!
}

View File

@@ -0,0 +1,43 @@
module imap
import net
import io
import freeflowuniverse.herolib.ui.console
// handle_authenticate processes the AUTHENTICATE command
pub fn (mut self Session) handle_authenticate(tag string, parts []string) ! {
if parts.len < 3 {
conn.write('${tag} BAD AUTHENTICATE requires an authentication mechanism\r\n'.bytes())!
return
}
auth_type := parts[2].to_upper()
if auth_type == 'PLAIN' {
// Send continuation request for credentials
conn.write('+ \r\n'.bytes())!
// Read base64 credentials
creds := reader.read_line() or {
match err.msg() {
'closed' {
console.print_debug('Client disconnected during authentication')
return error('client disconnected during auth')
}
'EOF' {
console.print_debug('Client ended connection during authentication (EOF)')
return error('connection ended during auth')
}
else {
eprintln('Connection read error during authentication: $err')
return error('connection error during auth: $err')
}
}
}
if creds.len > 0 {
// For demo purposes, accept any credentials
conn.write('${tag} OK [CAPABILITY IMAP4rev1 AUTH=PLAIN] Authentication successful\r\n'.bytes())!
} else {
conn.write('${tag} NO Authentication failed\r\n'.bytes())!
}
} else {
conn.write('${tag} NO [ALERT] Unsupported authentication mechanism\r\n'.bytes())!
}
}

View File

@@ -0,0 +1,49 @@
module imap
import net
import strconv
// handle_fetch processes the FETCH command
pub fn (mut self Session) handle_fetch( tag string, parts []string) ! {
mut mailbox:=self.mailbox()!
// For simplicity, we support commands like: A001 FETCH 1:* BODY[TEXT]
if parts.len < 4 {
conn.write('${tag} BAD FETCH requires a message sequence and data item\r\n'.bytes())!
return
}
sequence := parts[2]
selected_mailbox := server.mailboxes[mailbox_name]
// If the sequence is 1:*, iterate over all messages.
if sequence == '1:*' {
for i, msg in selected_mailbox.messages {
flags_str := if msg.flags.len > 0 {
'(' + msg.flags.join(' ') + ')'
} else {
'()'
}
// In a full implementation, more attributes would be returned.
conn.write('* ${i+1} FETCH (FLAGS ${flags_str} BODY[TEXT] "${msg.body}")\r\n'.bytes())!
}
conn.write('${tag} OK FETCH completed\r\n'.bytes())!
return true
} else {
// Otherwise, parse a single message number
index := strconv.atoi(parts[2]) or {
conn.write('${tag} BAD Invalid message number\r\n'.bytes())!
return
} - 1
if index < 0 || index >= server.mailboxes[mailbox_name].messages.len {
conn.write('${tag} BAD Invalid message sequence\r\n'.bytes())!
} else {
msg := selected_mailbox.messages[index]
flags_str := if msg.flags.len > 0 {
'(' + msg.flags.join(' ') + ')'
} else {
'()'
}
conn.write('* ${index+1} FETCH (FLAGS ${flags_str} BODY[TEXT] "${msg.body}")\r\n'.bytes())!
conn.write('${tag} OK FETCH completed\r\n'.bytes())!
return true
}
}
}

View File

@@ -0,0 +1,13 @@
module imap
import net
// handle_login processes the LOGIN command
pub fn (mut self Session) handle_login(mut conn net.TcpConn, tag string, parts []string, server &IMAPServer) ! {
if parts.len < 4 {
conn.write('${tag} BAD LOGIN requires username and password\r\n'.bytes())!
return
}
// For demo purposes, accept any username/password
conn.write('${tag} OK [CAPABILITY IMAP4rev1 AUTH=PLAIN] User logged in\r\n'.bytes())!
}

View File

@@ -0,0 +1,9 @@
module imap
import net
// handle_logout processes the LOGOUT command
pub fn (mut self Session) handle_logout(mut conn net.TcpConn, tag string) ! {
conn.write('* BYE IMAP4rev1 Server logging out\r\n'.bytes())!
conn.write('${tag} OK LOGOUT completed\r\n'.bytes())!
}

View File

@@ -0,0 +1,27 @@
module imap
import net
// handle_select processes the SELECT command
pub fn (mut self Session) handle_select(mut conn net.TcpConn, tag string, parts []string, mut server IMAPServer) !string {
if parts.len < 3 {
conn.write('${tag} BAD SELECT requires a mailbox name\r\n'.bytes())!
return error('SELECT requires a mailbox name')
}
// Remove any surrounding quotes from mailbox name
mailbox_name := parts[2].trim('"')
// Look for the mailbox. If not found, create it.
if mailbox_name !in server.mailboxes {
server.mailboxes[mailbox_name] = &Mailbox{
name: mailbox_name
messages: []Message{}
}
}
// Respond with a basic status.
messages_count := server.mailboxes[mailbox_name].messages.len
conn.write('* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n'.bytes())!
conn.write('* ${messages_count} EXISTS\r\n'.bytes())!
conn.write('* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted\r\n'.bytes())!
conn.write('${tag} OK [READ-WRITE] SELECT completed\r\n'.bytes())!
return mailbox_name
}

View File

@@ -0,0 +1,61 @@
module imap
import net
import strconv
// handle_store processes the STORE command
pub fn (mut self Session) handle_store(mut conn net.TcpConn, tag string, parts []string, mut server IMAPServer, mailbox_name string) !bool {
if mailbox_name !in server.mailboxes {
conn.write('${tag} BAD No mailbox selected\r\n'.bytes())!
return
}
// Expecting a format like: A003 STORE 1 +FLAGS (\Seen)
if parts.len < 5 {
conn.write('${tag} BAD STORE requires a message sequence, an operation, and flags\r\n'.bytes())!
return
}
// For simplicity, only support a single message number.
index := strconv.atoi(parts[2]) or {
conn.write('${tag} BAD Invalid message number\r\n'.bytes())!
return
} - 1
if index < 0 || index >= server.mailboxes[mailbox_name].messages.len {
conn.write('${tag} BAD Invalid message sequence\r\n'.bytes())!
return
}
op := parts[3] // e.g. "+FLAGS", "-FLAGS", or "FLAGS"
// The flags are provided in the next token, e.g.: (\Seen)
flags_str := parts[4]
// Remove any surrounding parentheses.
flags_clean := flags_str.trim('()')
flags_arr := flags_clean.split(' ').filter(it != '')
mut msg := server.mailboxes[mailbox_name].messages[index]
match op {
'+FLAGS' {
// Add each flag if it isn't already present.
for flag in flags_arr {
if flag !in msg.flags {
msg.flags << flag
}
}
}
'-FLAGS' {
// Remove any flags that match.
for flag in flags_arr {
msg.flags = msg.flags.filter(it != flag)
}
}
'FLAGS' {
// Replace current flags.
msg.flags = flags_arr
}
else {
conn.write('${tag} BAD Unknown STORE operation\r\n'.bytes())!
return
}
}
// Save the updated message back.
server.mailboxes[mailbox_name].messages[index] = msg
conn.write('${tag} OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)] Store completed\r\n'.bytes())!
return true
}

View File

@@ -0,0 +1,31 @@
module imap
pub fn start()! {
// Create the server and initialize an example INBOX.
mut server := IMAPServer{
mailboxes: map[string]&Mailbox{}
}
mut inbox := Mailbox{
name: 'INBOX'
messages: [
Message{
id: 1
subject: 'Welcome'
body: 'Welcome to the IMAP server!'
flags: ['\\Seen']
},
Message{
id: 2
subject: 'Update'
body: 'This is an update.'
flags: []
},
]
}
// Store a pointer to the INBOX.
server.mailboxes['INBOX'] = &inbox
// Start the server (listening on port 143).
server.run() or { eprintln('Server error: $err') }
}

47
lib/servers/imap/model.v Normal file
View File

@@ -0,0 +1,47 @@
module imap
import net
import io
// Represents an email message.
pub struct Message {
pub mut:
id int
subject string
body string
flags []string // e.g.: ["\\Seen", "\\Flagged"]
}
// Represents a mailbox holding messages.
pub struct Mailbox {
pub mut:
name string
messages []Message
}
// Our in-memory server holds a map of mailbox names to pointers to Mailbox.
pub struct IMAPServer {
pub mut:
mailboxes map[string]&Mailbox
}
pub struct Session {
pub mut:
server &IMAPServer
mailbox string //the name of the mailbox
conn net.TcpConn
reader &io.BufferedReader
}
pub fn (mut self Session) mailbox_new(name string) !&Mailbox{
self.mailboxes[name] = &Mailbox{name:name}
return self.mailboxes[name]
}
pub fn (mut self Session) mailbox() !&Mailbox{
if !(mailbox_name in server.mailboxes) {
return error ("mailbox ${self.mailbox} does not exist")
}
return self.mailboxes[self.mailbox] or { panic(err) }
}

122
lib/servers/imap/server.v Normal file
View File

@@ -0,0 +1,122 @@
module imap
import net
import strings
import freeflowuniverse.herolib.ui.console
import time
import io
// Run starts the server on port 143 and accepts client connections.
fn (mut server IMAPServer) run() ! {
addr := '0.0.0.0:143'
mut listener := net.listen_tcp(.ip, addr, dualstack:true) or {
return error('Failed to listen on $addr: $err')
}
println('IMAP Server listening on $addr')
// Set TCP options for better reliability
// listener.set_option_bool(.reuse_addr, true)
for {
mut conn := listener.accept() or {
eprintln('Failed to accept connection: $err')
continue
}
// Set connection options
// conn.set_option_int(.tcp_keepalive, 60)!
conn.set_read_timeout(30 * time.second)
conn.set_write_timeout(30 * time.second)
// Handle each connection concurrently
spawn handle_connection(mut conn, mut server)
}
}
// handle_connection processes commands from a connected client.
fn handle_connection(mut conn net.TcpConn, mut server IMAPServer) ! {
// Send greeting per IMAP protocol.
defer {
conn.close() or { panic(err) }
}
conn.write('* OK [CAPABILITY IMAP4rev1 AUTH=PLAIN STARTTLS LOGIN] IMAP server ready\r\n'.bytes())!
// Initially no mailbox is selected.
mut selected_mailbox_name := ''
mut res := false
client_addr := conn.peer_addr()!
console.print_debug('> new client: ${client_addr}')
mut reader := io.new_buffered_reader(reader: conn)
defer {
unsafe {
reader.free()
}
}
mut session:= Session{
server:&server
mailbox:""
conn:conn
reader:reader
}
for {
// Read a line (command) from the client.
line := reader.read_line() or {
match err.msg() {
'closed' {
console.print_debug('Client disconnected normally')
return error('client disconnected')
}
'EOF' {
console.print_debug('Client connection ended (EOF)')
return error('connection ended')
}
else {
eprintln('Connection read error: $err')
return error('connection error: $err')
}
}
}
console.print_debug(line)
trimmed := line.trim_space()
if trimmed.len == 0 {
continue
}
// Commands come with a tag followed by the command and parameters.
parts := trimmed.split(' ')
if parts.len < 2 {
conn.write('${parts[0]} BAD Invalid command\r\n'.bytes())!
continue
}
tag := parts[0]
cmd := parts[1].to_upper()
match cmd {
'LOGIN' {
session.handle_login(tag, parts )!
}
'AUTHENTICATE' {
session.handle_authenticate(tag, parts)!
}
'SELECT' {
session.selected_mailbox_name = handle_select(mut conn, tag, parts, mut server) !
}
'FETCH' {
session.handle_fetch(mut conn, tag, parts) !
}
'STORE' {
session.handle_store(mut conn, tag, parts) !
}
'CAPABILITY' {
session.handle_capability(mut conn, tag) !
}
'LOGOUT' {
handle_logout(mut conn, tag) !
return
}
else {
conn.write('${tag} BAD Unknown command\r\n'.bytes())!
}
}
}
}

View File

@@ -0,0 +1,60 @@
The Internet Message Access Protocol Version 4 Revision 1 (IMAP4rev1) enables clients to access and manage email messages on a server, treating remote mailboxes as if they were local folders. This protocol supports operations such as creating, deleting, and renaming mailboxes; checking for new messages; permanently removing messages; setting and clearing flags; parsing messages; searching; and selectively fetching message attributes and content. IMAP4rev1 is designed for single-server access and does not include functionality for sending emails, which is typically handled by protocols like SMTP.
## Introduction
IMAP4rev1 allows clients to interact with email messages on a server, providing functionalities similar to local email management. It enables seamless synchronization between clients and servers, facilitating both online and offline email access.
## Protocol Overview
IMAP4rev1 operates over a reliable data stream, typically TCP. Communication involves clients sending commands to the server and receiving responses. Servers can also send untagged responses to inform clients of real-time updates.
## Commands and Responses
- **Commands**: Issued by the client to perform actions like selecting a mailbox or fetching messages.
- **Responses**: Returned by the server to indicate the status of a command or to provide requested data.
Commands are categorized as follows:
- **Authentication Commands**: Manage client authentication.
- **Mailbox Commands**: Handle mailbox selection and status.
- **Message Commands**: Operate on messages within a mailbox.
## Mailbox Operations
Clients can perform various operations on mailboxes, including:
- **Create**: Establish a new mailbox.
- **Delete**: Remove an existing mailbox.
- **Rename**: Change the name of a mailbox.
- **Subscribe/Unsubscribe**: Manage mailbox subscriptions.
- **List**: Retrieve a list of mailboxes.
- **Status**: Obtain the status of a mailbox, such as message count.
## Message Operations
Within mailboxes, clients can:
- **Fetch**: Retrieve specific message data.
- **Search**: Find messages matching certain criteria.
- **Store**: Alter message attributes, like flags.
- **Copy**: Duplicate messages to another mailbox.
- **Expunge**: Permanently remove messages marked for deletion.
## Client and Server Responsibilities
- **Client**: Initiates commands, processes server responses, and maintains synchronization with the server.
- **Server**: Executes client commands, sends appropriate responses, and manages the state of mailboxes and messages.
## Security Considerations
IMAP4rev1 does not inherently provide encryption. It's essential to implement security measures such as TLS to protect data transmitted between clients and servers.
## References
- **IMAP4rev1 Specification**: [RFC 3501](https://datatracker.ietf.org/doc/html/rfc3501)
- **Email Message Format**: [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822)
- **MIME Standards**: [RFC 2045](https://datatracker.ietf.org/doc/html/rfc2045)
- **Multiple Mailbox Support**: [RFC 2244](https://datatracker.ietf.org/doc/html/rfc2244)
- **Mail Transfer Protocol**: [RFC 2821](https://datatracker.ietf.org/doc/html/rfc2821)
For a comprehensive understanding and detailed technical specifications, refer to the full [RFC 3501 document](https://datatracker.ietf.org/doc/html/rfc3501).

View File

@@ -0,0 +1,191 @@
#!/bin/bash
set -euo pipefail
###############################################################################
# WARNING: THIS SCRIPT ERASES DATA!
#
# This script will:
# 1. Identify all internal (nonremovable) SSD and NVMe disks, excluding the
# live USB from which Ubuntu is booted.
# 2. Wipe their partition tables.
# 3. On the first detected disk, create:
# - a 1GB EFI partition (formatted FAT32, flagged as ESP)
# - a ~19GB partition for Ubuntu root (formatted ext4)
# - if any space remains, a partition covering the rest (formatted btrfs)
# 4. On every other disk, create one partition spanning the entire disk and
# format it as btrfs.
#
# Doublecheck that you want to wipe all these disks BEFORE you run this script.
###############################################################################
# Ensure the script is run as root.
if [ "$EUID" -ne 0 ]; then
echo "This script must be run as root."
exit 1
fi
# Helper: given a device like /dev/sda1 or /dev/nvme0n1p1, return the base device.
get_base_device() {
local dev="$1"
if [[ "$dev" =~ ^/dev/nvme.*p[0-9]+$ ]]; then
# For NVMe devices, remove the trailing 'pX'
echo "$dev" | sed -E 's/p[0-9]+$//'
else
# For /dev/sdX type devices, remove trailing numbers
echo "$dev" | sed -E 's/[0-9]+$//'
fi
}
# Helper: given a disk (e.g. /dev/sda or /dev/nvme0n1) and a partition number,
# print the proper partition name.
get_partition_name() {
local disk="$1"
local partnum="$2"
if [[ "$disk" =~ nvme ]]; then
echo "${disk}p${partnum}"
else
echo "${disk}${partnum}"
fi
}
# Determine the boot device (i.e. the device from which the live system is running)
boot_dev_full=$(findmnt -n -o SOURCE /)
boot_disk=$(get_base_device "$boot_dev_full")
echo "Detected boot device (from /): $boot_dev_full"
echo "Base boot disk (will be used for Ubuntu install): $boot_disk"
# Now, enumerate candidate target disks.
# We will scan /sys/block for devices starting with "sd" or "nvme".
target_disks=()
# Loop over sd* and nvme* disks.
for dev_path in /sys/block/sd* /sys/block/nvme*; do
[ -e "$dev_path" ] || continue
disk_name=$(basename "$dev_path")
disk="/dev/$disk_name"
# Skip removable devices (e.g. USB sticks)
if [ "$(cat "$dev_path/removable")" -ne 0 ]; then
continue
fi
# Skip disks that are rotational (i.e. likely HDD) if you want only SSD/NVMe.
# (Usually SSD/NVMe have rotational=0.)
if [ -f "$dev_path/queue/rotational" ]; then
if [ "$(cat "$dev_path/queue/rotational")" -ne 0 ]; then
continue
fi
fi
# Add disk to list.
target_disks+=("$disk")
done
# Ensure the boot disk is in our list. (It will be partitioned for Ubuntu.)
if [[ ! " ${target_disks[@]} " =~ " ${boot_disk} " ]]; then
# Check if boot_disk qualifies (nonremovable and nonrotational)
disk_dir="/sys/block/$(basename "$boot_disk")"
if [ -f "$disk_dir/removable" ] && [ "$(cat "$disk_dir/removable")" -eq 0 ]; then
if [ -f "$disk_dir/queue/rotational" ] && [ "$(cat "$disk_dir/queue/rotational")" -eq 0 ]; then
target_disks=("$boot_disk" "${target_disks[@]}")
fi
fi
fi
if [ "${#target_disks[@]}" -eq 0 ]; then
echo "No qualifying internal SSD/NVMe disks found."
exit 1
fi
echo
echo "The following disks will be wiped and re-partitioned:"
for disk in "${target_disks[@]}"; do
echo " $disk"
done
echo
read -p "ARE YOU SURE YOU WANT TO PROCEED? This will permanently erase all data on these disks (type 'yes' to continue): " answer
if [ "$answer" != "yes" ]; then
echo "Aborting."
exit 1
fi
###############################################################################
# Wipe all target disks.
###############################################################################
for disk in "${target_disks[@]}"; do
echo "Wiping partition table on $disk..."
sgdisk --zap-all "$disk"
# Overwrite beginning of disk (optional but recommended)
dd if=/dev/zero of="$disk" bs=512 count=2048 status=none
# Overwrite end of disk (ignoring errors if size is too small)
total_sectors=$(blockdev --getsz "$disk")
dd if=/dev/zero of="$disk" bs=512 count=2048 seek=$(( total_sectors - 2048 )) status=none 2>/dev/null || true
done
###############################################################################
# Partition the FIRST disk for Ubuntu installation.
###############################################################################
boot_install_disk="${target_disks[0]}"
echo
echo "Partitioning boot/install disk: $boot_install_disk"
parted -s "$boot_install_disk" mklabel gpt
# Create EFI partition: from 1MiB to 1025MiB (~1GB).
parted -s "$boot_install_disk" mkpart ESP fat32 1MiB 1025MiB
parted -s "$boot_install_disk" set 1 esp on
# Create root partition: from 1025MiB to 21025MiB (~20GB total for install).
parted -s "$boot_install_disk" mkpart primary ext4 1025MiB 21025MiB
# Determine if theres any space left.
disk_size_bytes=$(blockdev --getsize64 "$boot_install_disk")
# Calculate 21025MiB in bytes.
min_install_bytes=$((21025 * 1024 * 1024))
if [ "$disk_size_bytes" -gt "$min_install_bytes" ]; then
echo "Creating additional partition on $boot_install_disk for btrfs (using remaining space)..."
parted -s "$boot_install_disk" mkpart primary btrfs 21025MiB 100%
boot_disk_partitions=(1 2 3)
else
boot_disk_partitions=(1 2)
fi
# Format the partitions on the boot/install disk.
efi_part=$(get_partition_name "$boot_install_disk" 1)
root_part=$(get_partition_name "$boot_install_disk" 2)
echo "Formatting EFI partition ($efi_part) as FAT32..."
mkfs.fat -F32 "$efi_part"
echo "Formatting root partition ($root_part) as ext4..."
mkfs.ext4 -F "$root_part"
# If a third partition exists, format it as btrfs.
if [ "${boot_disk_partitions[2]:-}" ]; then
btrfs_part=$(get_partition_name "$boot_install_disk" 3)
echo "Formatting extra partition ($btrfs_part) as btrfs..."
mkfs.btrfs -f "$btrfs_part"
fi
###############################################################################
# Partition all OTHER target disks entirely as btrfs.
###############################################################################
if [ "${#target_disks[@]}" -gt 1 ]; then
echo
echo "Partitioning remaining disks for btrfs:"
for disk in "${target_disks[@]:1}"; do
echo "Processing disk $disk..."
parted -s "$disk" mklabel gpt
parted -s "$disk" mkpart primary btrfs 1MiB 100%
# Determine the partition name (e.g. /dev/sdb1 or /dev/nvme0n1p1).
if [[ "$disk" =~ nvme ]]; then
part="${disk}p1"
else
part="${disk}1"
fi
echo "Formatting $part as btrfs..."
mkfs.btrfs -f "$part"
done
fi
echo
echo "All operations complete. Ubuntu install partitions and btrfs volumes have been created."