diff --git a/examples/servers/imap_example.vsh b/examples/servers/imap_example.vsh new file mode 100755 index 00000000..58dd2dde --- /dev/null +++ b/examples/servers/imap_example.vsh @@ -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) +} \ No newline at end of file diff --git a/lib/servers/imap/README.md b/lib/servers/imap/README.md new file mode 100644 index 00000000..9a16ffcd --- /dev/null +++ b/lib/servers/imap/README.md @@ -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 diff --git a/lib/servers/imap/capability.v b/lib/servers/imap/capability.v new file mode 100644 index 00000000..70ff994e --- /dev/null +++ b/lib/servers/imap/capability.v @@ -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())! +} diff --git a/lib/servers/imap/cmd_authenticate.v b/lib/servers/imap/cmd_authenticate.v new file mode 100644 index 00000000..7b9af349 --- /dev/null +++ b/lib/servers/imap/cmd_authenticate.v @@ -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())! + } +} diff --git a/lib/servers/imap/cmd_fetch.v b/lib/servers/imap/cmd_fetch.v new file mode 100644 index 00000000..a6cdf3c0 --- /dev/null +++ b/lib/servers/imap/cmd_fetch.v @@ -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 + } + } +} diff --git a/lib/servers/imap/cmd_login.v b/lib/servers/imap/cmd_login.v new file mode 100644 index 00000000..d19a7364 --- /dev/null +++ b/lib/servers/imap/cmd_login.v @@ -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())! +} diff --git a/lib/servers/imap/cmd_logout.v b/lib/servers/imap/cmd_logout.v new file mode 100644 index 00000000..dca58f06 --- /dev/null +++ b/lib/servers/imap/cmd_logout.v @@ -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())! +} diff --git a/lib/servers/imap/cmd_select.v b/lib/servers/imap/cmd_select.v new file mode 100644 index 00000000..390e132f --- /dev/null +++ b/lib/servers/imap/cmd_select.v @@ -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 +} diff --git a/lib/servers/imap/cmd_store.v b/lib/servers/imap/cmd_store.v new file mode 100644 index 00000000..ff0c726d --- /dev/null +++ b/lib/servers/imap/cmd_store.v @@ -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 +} diff --git a/lib/servers/imap/factory.v b/lib/servers/imap/factory.v new file mode 100644 index 00000000..ba5e187a --- /dev/null +++ b/lib/servers/imap/factory.v @@ -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') } +} diff --git a/lib/servers/imap/model.v b/lib/servers/imap/model.v new file mode 100644 index 00000000..37c38c87 --- /dev/null +++ b/lib/servers/imap/model.v @@ -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) } + +} \ No newline at end of file diff --git a/lib/servers/imap/server.v b/lib/servers/imap/server.v new file mode 100644 index 00000000..b0083158 --- /dev/null +++ b/lib/servers/imap/server.v @@ -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())! + } + } + } +} diff --git a/lib/servers/imap/specs/imap_3501.md b/lib/servers/imap/specs/imap_3501.md new file mode 100644 index 00000000..15374d80 --- /dev/null +++ b/lib/servers/imap/specs/imap_3501.md @@ -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). \ No newline at end of file diff --git a/research/globals/ubuntu_partition.sh b/research/globals/ubuntu_partition.sh new file mode 100644 index 00000000..cb61cb75 --- /dev/null +++ b/research/globals/ubuntu_partition.sh @@ -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 1 GB EFI partition (formatted FAT32, flagged as ESP) +# - a ~19 GB 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. +# +# Double‐check 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 there’s 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."