diff --git a/docs/superpowers/contracts/linux-config-giants-2026-08-16.json b/docs/superpowers/contracts/linux-config-giants-2026-08-16.json new file mode 100644 index 00000000..f5978b39 --- /dev/null +++ b/docs/superpowers/contracts/linux-config-giants-2026-08-16.json @@ -0,0 +1,18 @@ +{ + "title": "Split three more very large linux_configuration scripts", + "objective": "Bring raspberry_pi_nextcloud.sh (1276), root_bl9000.sh (1116) and lookup_docs.sh (942) under the 250-line cap, fixing the cd handling the splits expose rather than suppressing it.", + "acceptance_criteria": [ + "All three entry scripts and all sixteen new libs are at most 250 lines.", + "The pre-commit shellcheck hook passes on every touched file, with no new suppression and no disable directive.", + "No lib sources a sibling: each entry script sources every lib directly, in dependency order.", + "The stubbed run reports every moved function as defined, for all three scripts.", + "Every SC2164 and SC2103 the split exposes is fixed at the source. Where a function exports a variable to its caller it must not become a subshell; guard the cd and restore the previous directory instead.", + "No script is executed. These flash SD cards, root a phone over USB and configure a web server." + ], + "out_of_scope": [ + "Executing any flash, root or configure step.", + "check_and_enable_services.sh and the other deferred scripts, whose seams cross shared mutable state.", + "Changing what any script does when run, beyond the cd guards." + ], + "verifier": "meta/scripts/verify_shell_split.sh per script, plus shellcheck over every lib in isolation, plus a grep confirming no lib sources a sibling, plus pre-commit run --files and check_file_length.sh." +} diff --git a/docs/superpowers/evidence/linux-config-giants-2026-08-16.json b/docs/superpowers/evidence/linux-config-giants-2026-08-16.json new file mode 100644 index 00000000..f8cda613 --- /dev/null +++ b/docs/superpowers/evidence/linux-config-giants-2026-08-16.json @@ -0,0 +1,47 @@ +{ + "intent": "Split three more very large linux_configuration scripts under the cap, and fix the directory handling the splits exposed.", + "scope": [ + "single_use/features/raspberry_pi_nextcloud.sh (1276 -> 163) + 7 libs", + "single_use/utils/root_bl9000.sh (1116 -> 211) + 6 libs", + "single_use/utils/lookup_docs.sh (942 -> 165) + 6 libs", + "Non-goal: no script executed" + ], + "changes": [ + "Sixteen new libs across the three scripts. raspberry_pi_nextcloud and root_bl9000 each needed a second carve after the first pass left a lib over the cap.", + "lookup_docs.sh's lookup_python was a single 300-line function. Extracted the keyword-to-anchor mapping into python_keyword_doc, which emits result and desc tab-separated so the caller keeps its own control flow - the block only ever assigned those two variables.", + "Fixed four more unguarded cd calls in the Nextcloud libs, each before an occ command or a download.", + "bl9000_mtkclient.sh had the cd X; work; cd - pattern with three separate return paths. install_mtkclient became a subshell. extract_boot_with_mtkclient could NOT: it exports BOOT_IMG to its caller, so a subshell would swallow it. Instead the venv check moved to before the cd (so that early return needs no cd back), the cd is guarded, and the restores use a saved $PWD rather than cd -.", + "Flattened two chained source blocks. The root_bl9000 usage split had absorbed the entry's whole source block into a lib, and the docs split had done the same. Both entries now source every lib directly.", + "lookup_docs.sh had no SCRIPT_DIR at all - it needed one adding before its new source block." + ], + "verification": [ + { + "command": "meta/scripts/verify_shell_split.sh per script", + "result": "fail then pass", + "evidence": "Three rounds of failures, each a real defect: SC2164 on the unguarded cd calls, SC1091 from the chained source blocks, and then '/lib/docs_python_keywords.sh: No such file' because SCRIPT_DIR was undefined. All three report 'libs sourced, functions defined' now." + }, + { + "command": "shellcheck over every lib in isolation", + "result": "pass", + "evidence": "Clean, including SC2103, with no disable directive anywhere." + }, + { + "command": "grep -l 'source \"$SCRIPT_DIR/lib/' over all libs in the touched directories", + "result": "pass", + "evidence": "No matches - no lib sources a sibling. This is the third occurrence of the chaining bug, so it is now checked explicitly rather than noticed." + }, + { + "command": "bash ~/utils/scripts/check_file_length.sh --all", + "result": "pass", + "evidence": "None of the three entry scripts or sixteen libs is listed." + } + ], + "risks": [ + "None of the three was run: they flash SD cards, put a phone into BROM mode over USB, and configure Apache and MariaDB. The cd fixes change failure paths only.", + "extract_boot_with_mtkclient still uses cd rather than a subshell, deliberately, because it exports BOOT_IMG. The restore is now unconditional on both exit paths, but a future edit adding a third return must restore too - a subshell would make that impossible to get wrong, at the cost of the exported variable." + ], + "rollback": [ + "git revert the commit; the three scripts return to their previous sizes and the sixteen libs disappear.", + "After rollback, re-run bash -n and shellcheck on the restored scripts." + ] +} diff --git a/linux_configuration/scripts/single_use/features/lib/rpi_nc_ca.sh b/linux_configuration/scripts/single_use/features/lib/rpi_nc_ca.sh new file mode 100755 index 00000000..8226fa40 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/rpi_nc_ca.sh @@ -0,0 +1,179 @@ +#!/bin/bash +# CA certificate installation. +# +# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ============================================================================= +# Install CA Certificate on Client +# ============================================================================= + +phase_install_ca() { + log_info "=== Installing Nextcloud CA Certificate ===" + + if [[ -z $PI_PASSWORD ]]; then + die "PI_PASSWORD not set. Run this after running install-remote or flash." + fi + + local pi_ip + pi_ip=$(discover_raspberry_pi) + + if [[ -z $pi_ip ]]; then + die "Failed to discover Raspberry Pi" + fi + + log_info "Downloading CA certificate from Pi..." + + local ca_file="/tmp/nextcloud-ca.crt" + + # Use SSH with sudo to cat the file (since it's in a protected directory) + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no \ + "${PI_USER}@${pi_ip}" "echo '$PI_PASSWORD' | sudo -S cat /etc/ssl/nextcloud/ca.crt" >"$ca_file" 2>/dev/null + + if [[ ! -f $ca_file ]] || [[ ! -s $ca_file ]]; then + die "Failed to download CA certificate" + fi + + log_success "CA certificate downloaded to: $ca_file" + + # Detect OS and install appropriately + if [[ -f /etc/arch-release ]]; then + log_info "Detected Arch Linux - installing CA..." + sudo cp "$ca_file" /etc/ca-certificates/trust-source/anchors/nextcloud-ca.crt + sudo trust extract-compat + log_success "CA installed in system trust store" + + elif [[ -f /etc/debian_version ]]; then + log_info "Detected Debian/Ubuntu - installing CA..." + sudo cp "$ca_file" /usr/local/share/ca-certificates/nextcloud-ca.crt + sudo update-ca-certificates + log_success "CA installed in system trust store" + + elif [[ -f /etc/redhat-release ]]; then + log_info "Detected RHEL/Fedora - installing CA..." + sudo cp "$ca_file" /etc/pki/ca-trust/source/anchors/nextcloud-ca.crt + sudo update-ca-trust + log_success "CA installed in system trust store" + + elif [[ "$(uname)" == "Darwin" ]]; then + log_info "Detected macOS - installing CA..." + sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$ca_file" + log_success "CA installed in system keychain" + + else + log_warning "Unknown OS - please install CA manually from: $ca_file" + fi + + # Install in browser certificate stores + log_info "Installing CA in browser certificate stores..." + + # Chrome/Chromium (uses NSS) + if [[ -d ~/.pki/nssdb ]] || command -v certutil &>/dev/null; then + mkdir -p ~/.pki/nssdb + if ! certutil -d sql:~/.pki/nssdb -L 2>/dev/null | grep -q "Nextcloud"; then + # Initialize NSS db if needed + certutil -d sql:~/.pki/nssdb -N --empty-password 2>/dev/null || true + if certutil -d sql:~/.pki/nssdb -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null; then + log_success "CA installed in Chrome/Chromium" + else + log_warning "Could not install in Chrome/Chromium NSS db" + fi + else + log_info "CA already installed in Chrome/Chromium" + fi + fi + + # Firefox (has its own profile NSS databases) + if [[ -d ~/.mozilla/firefox ]]; then + local installed=0 + for profile_dir in ~/.mozilla/firefox/*.default* ~/.mozilla/firefox/*.esr*; do + if [[ -d $profile_dir ]]; then + if ! certutil -d sql:"$profile_dir" -L 2>/dev/null | grep -q "Nextcloud"; then + certutil -d sql:"$profile_dir" -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null && + installed=1 + else + installed=1 + fi + fi + done + if [[ $installed -eq 1 ]]; then + log_success "CA installed in Firefox" + else + log_warning "Could not install in Firefox - you may need to import manually" + fi + fi + + # Add hostname to /etc/hosts if not present + if ! grep -q "$PI_HOSTNAME" /etc/hosts 2>/dev/null; then + log_info "Adding $PI_HOSTNAME to /etc/hosts..." + echo "$pi_ip $PI_HOSTNAME ${PI_HOSTNAME}.local" | sudo tee -a /etc/hosts >/dev/null + log_success "Added $PI_HOSTNAME to /etc/hosts" + else + log_info "$PI_HOSTNAME already in /etc/hosts" + fi + + # Verify + log_info "Verifying HTTPS connection..." + if curl -s --max-time 5 "https://$PI_HOSTNAME/status.php" 2>/dev/null | grep -q "installed"; then + log_success "HTTPS connection verified - no certificate warnings!" + else + log_warning "Could not verify HTTPS - you may need to restart your browser" + fi + + log_success "========================================" + log_success "CA Certificate installed!" + log_success "========================================" + echo + log_info "Access Nextcloud at: https://$PI_HOSTNAME" + log_info "Your browser should now trust the certificate without warnings." + echo + log_info "For other devices (phones, tablets, other computers):" + log_info " Download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" + log_info " Then install the certificate in your device's trust store." +} + +# ============================================================================= +# Main +# ============================================================================= + +show_help() { + cat <<'EOF' +Nextcloud Installation Script for Raspberry Pi + +Usage: ./raspberry_pi_nextcloud.sh + +Commands: + install-remote Install Nextcloud via SSH from your laptop (recommended) + setup-ssl-remote Setup Let's Encrypt SSL with DuckDNS (auto-trusted on all devices) + install-ca Install self-signed CA on this machine (alternative to setup-ssl) + configure Configure Pi system (run on Pi) + install-local Install Nextcloud (run on Pi) + fix Fix common Nextcloud issues (run on Pi) + setup-ssl Setup Let's Encrypt SSL (run on Pi) + help Show this help message + +The script will: +1. Configure the Raspberry Pi system (SSH hardening, firewall, etc.) +2. Install Apache, PHP, MariaDB, Redis +3. Download and install Nextcloud +4. Configure caching, background jobs, and security + +For HTTPS trusted on ALL devices automatically: + ./raspberry_pi_nextcloud.sh install-remote + ./raspberry_pi_nextcloud.sh setup-ssl-remote + + This uses DuckDNS (free) + Let's Encrypt for real trusted certificates. + Go to https://www.duckdns.org/ to get your free domain first. + +For self-signed certificates (requires manual CA install on each device): + ./raspberry_pi_nextcloud.sh install-remote + ./raspberry_pi_nextcloud.sh install-ca + +For local installation (on Pi): + sudo ./raspberry_pi_nextcloud.sh configure + sudo ./raspberry_pi_nextcloud.sh install-local + sudo ./raspberry_pi_nextcloud.sh fix + sudo ./raspberry_pi_nextcloud.sh setup-ssl +EOF +} diff --git a/linux_configuration/scripts/single_use/features/lib/rpi_nc_deps.sh b/linux_configuration/scripts/single_use/features/lib/rpi_nc_deps.sh new file mode 100755 index 00000000..e59117e7 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/rpi_nc_deps.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# Dependency install, apt lock wait and Pi discovery. +# +# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +wait_for_apt_lock() { + local max_wait=600 + local waited=0 + + while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do + if [[ $waited -eq 0 ]]; then + log_info "Waiting for other apt/dpkg processes to finish..." + pgrep -a 'apt|dpkg' | head -5 >&2 || true + fi + sleep 5 + waited=$((waited + 5)) + if [[ $waited -ge $max_wait ]]; then + die "Timeout waiting for apt lock after ${max_wait}s" + fi + if [[ $((waited % 30)) -eq 0 ]]; then + log_info "Still waiting... (${waited}s elapsed)" + fi + done + + if [[ $waited -gt 0 ]]; then + log_success "Apt lock acquired after ${waited}s" + fi +} + +# ============================================================================= +# Network Discovery (for remote installation) +# ============================================================================= + +ensure_dependencies() { + local missing_packages=() + + if ! command -v nmap &>/dev/null; then + missing_packages+=("nmap") + fi + + if ! command -v sshpass &>/dev/null; then + missing_packages+=("sshpass") + fi + + if [[ ${#missing_packages[@]} -gt 0 ]]; then + log_info "Installing missing packages: ${missing_packages[*]}" + + if command -v pacman &>/dev/null; then + sudo pacman -S --noconfirm "${missing_packages[@]}" + elif command -v apt-get &>/dev/null; then + sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" + elif command -v dnf &>/dev/null; then + sudo dnf install -y "${missing_packages[@]}" + else + die "Could not detect package manager. Please install manually: ${missing_packages[*]}" + fi + fi +} + +discover_raspberry_pi() { + log_info "Auto-discovering Raspberry Pi on local network..." + + ensure_dependencies + + local my_ip + my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -1) + local network="${gateway%.*}.0/24" + + log_info "Local IP: $my_ip, Network: $network" + log_info "Scanning for Raspberry Pi (hostname: $PI_HOSTNAME)..." + + local pi_ip="" + + # Try resolving hostname directly + pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true + if [[ -z $pi_ip ]]; then + pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true + fi + + if [[ -n $pi_ip ]]; then + log_success "Found Pi by hostname: $pi_ip" + echo "$pi_ip" + return + fi + + log_info "Hostname resolution failed, scanning network..." + nmap -sn -T4 "$network" &>/dev/null || true + + local ssh_hosts + ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) || true + + if [[ -z $ssh_hosts ]]; then + die "No SSH-enabled devices found. Is the Pi connected and booted?" + fi + + log_info "Found SSH-enabled devices: $(echo "$ssh_hosts" | tr '\n' ' ')" + + for ip in $ssh_hosts; do + log_info "Trying $ip with user '$PI_USER'..." + + if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "hostname" 2>/dev/null | grep -qi "$PI_HOSTNAME"; then + log_success "Found Raspberry Pi at $ip" + echo "$ip" + return + fi + + if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then + log_success "Found device responding to Pi credentials at $ip" + echo "$ip" + return + fi + done + + die "Could not find Raspberry Pi on network." +} diff --git a/linux_configuration/scripts/single_use/features/lib/rpi_nc_fixes.sh b/linux_configuration/scripts/single_use/features/lib/rpi_nc_fixes.sh new file mode 100755 index 00000000..85444eb9 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/rpi_nc_fixes.sh @@ -0,0 +1,233 @@ +#!/bin/bash +# The post-install fix-up phase. +# +# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ============================================================================= +# Fix Nextcloud Issues +# ============================================================================= + +# shellcheck disable=SC2120 # Function does not use positional args +phase_fix_issues() { + check_root + + log_info "=== Fixing Nextcloud Issues ===" + + cd /var/www/nextcloud || { log_error "Nextcloud is not installed at /var/www/nextcloud"; return 1; } + + # 1. Fix background jobs (cron not running properly) + log_info "Fixing background jobs..." + + # Ensure cron is set as background job method + sudo -u www-data php occ background:cron + + # Ensure cron job exists and is correct + ( + crontab -u www-data -l 2>/dev/null | grep -v "cron.php" + echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" + ) | crontab -u www-data - + + # Run cron manually now to reset the timer + log_info "Running cron job manually..." + sudo -u www-data php /var/www/nextcloud/cron.php + + log_success "Background jobs configured" + + # 2. Setup HTTPS with proper CA-signed certificate + log_info "Setting up HTTPS with trusted CA..." + + local pi_ip + pi_ip=$(hostname -I | awk '{print $1}') + + local ssl_dir="/etc/ssl/nextcloud" + mkdir -p "$ssl_dir" + chmod 700 "$ssl_dir" + + # Generate CA if it doesn't exist + if [[ ! -f "$ssl_dir/ca.crt" ]]; then + log_info "Creating Certificate Authority (CA)..." + + # Generate CA private key + openssl genrsa -out "$ssl_dir/ca.key" 4096 + chmod 600 "$ssl_dir/ca.key" + + # Generate CA certificate (valid for 10 years) + openssl req -x509 -new -nodes -key "$ssl_dir/ca.key" \ + -sha256 -days 3650 \ + -out "$ssl_dir/ca.crt" \ + -subj "/C=PL/ST=Home/L=Local/O=Nextcloud Home CA/OU=Certificate Authority/CN=Nextcloud Home CA" + + log_success "CA created: $ssl_dir/ca.crt" + fi + + # Generate server certificate signed by our CA + local regenerate="${1:-}" + if [[ ! -f "$ssl_dir/server.crt" ]] || [[ $regenerate == "--regenerate" ]]; then + log_info "Generating server certificate signed by CA..." + + # Generate server private key + openssl genrsa -out "$ssl_dir/server.key" 2048 + chmod 600 "$ssl_dir/server.key" + + # Create certificate signing request (CSR) + openssl req -new -key "$ssl_dir/server.key" \ + -out "$ssl_dir/server.csr" \ + -subj "/C=PL/ST=Home/L=Local/O=Nextcloud/OU=Server/CN=$PI_HOSTNAME" + + # Create extension file for SAN (Subject Alternative Names) + # This allows the certificate to be valid for hostname, IP, and .local + cat >"$ssl_dir/server.ext" </etc/apache2/sites-available/nextcloud-ssl.conf < + ServerAdmin admin@localhost + DocumentRoot /var/www/nextcloud + ServerName $PI_HOSTNAME + ServerAlias ${PI_HOSTNAME}.local $pi_ip + + SSLEngine on + SSLCertificateFile $ssl_dir/server.crt + SSLCertificateKeyFile $ssl_dir/server.key + + + Require all granted + AllowOverride All + Options FollowSymLinks MultiViews + + + Dav off + + + + # Security headers + Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains" + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "SAMEORIGIN" + Header always set X-XSS-Protection "1; mode=block" + Header always set Referrer-Policy "no-referrer" + + ErrorLog \${APACHE_LOG_DIR}/nextcloud_ssl_error.log + CustomLog \${APACHE_LOG_DIR}/nextcloud_ssl_access.log combined + + +# Redirect HTTP to HTTPS + + ServerName $PI_HOSTNAME + ServerAlias $pi_ip + Redirect permanent / https://$PI_HOSTNAME/ + +EOF + + a2enmod ssl + a2enmod headers + a2ensite nextcloud-ssl + + # Update Nextcloud config for HTTPS + sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$PI_HOSTNAME" + sudo -u www-data php occ config:system:set overwriteprotocol --value="https" + + systemctl restart apache2 + + log_success "HTTPS configured with CA-signed certificate" + + # 3. Run mimetype migrations + log_info "Running mimetype migrations..." + sudo -u www-data php occ maintenance:repair --include-expensive + log_success "Mimetype migrations complete" + + # 4. Add missing database indices + log_info "Adding missing database indices..." + sudo -u www-data php occ db:add-missing-indices + log_success "Database indices added" + + # 5. Install ImageMagick SVG support + log_info "Installing ImageMagick SVG support..." + DEBIAN_FRONTEND=noninteractive apt-get install -y libmagickcore-6.q16-6-extra + + # Enable SVG in ImageMagick policy + local policy_file="/etc/ImageMagick-6/policy.xml" + if [[ -f $policy_file ]]; then + # Remove SVG restrictions if present + sed -i 's///' "$policy_file" + # If no SVG policy exists, add one allowing it + if ! grep -q 'pattern="SVG"' "$policy_file"; then + sed -i '//a\ ' "$policy_file" + fi + fi + + systemctl restart apache2 + log_success "ImageMagick SVG support configured" + + # 6. Set up basic SMTP (placeholder - user needs to configure actual mail server) + log_info "Note: Email server not configured - please configure in Nextcloud admin settings" + + # 7. Clear any remaining warnings + log_info "Clearing Nextcloud caches..." + sudo -u www-data php occ maintenance:repair + sudo -u www-data php occ files:scan --all + + # 8. Verify all fixes + log_info "Verifying fixes..." + + # Run cron again to update last run time + sudo -u www-data php /var/www/nextcloud/cron.php + + log_success "========================================" + log_success "Nextcloud issues fixed!" + log_success "========================================" + echo + log_info "Summary of changes:" + log_info " ✓ Background jobs (cron) configured and running" + log_info " ✓ HTTPS enabled with CA-signed certificate" + log_info " ✓ Strict-Transport-Security header added" + log_info " ✓ Mimetype migrations completed" + log_info " ✓ Missing database indices added" + log_info " ✓ ImageMagick SVG support installed" + echo + log_info "Current certificate: self-signed CA (requires manual install on devices)" + log_info " - Run: $0 install-ca (on your laptop)" + log_info " - Or download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" + echo + log_info "For auto-trusted HTTPS on ALL devices (recommended):" + log_info " 1. Get free domain at https://www.duckdns.org/" + log_info " 2. Run: $0 setup-ssl" + echo + log_info "Access Nextcloud at: https://$PI_HOSTNAME" +} diff --git a/linux_configuration/scripts/single_use/features/lib/rpi_nc_install.sh b/linux_configuration/scripts/single_use/features/lib/rpi_nc_install.sh new file mode 100755 index 00000000..709b981b --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/rpi_nc_install.sh @@ -0,0 +1,228 @@ +#!/bin/bash +# The Nextcloud install phase. +# +# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ============================================================================= +# Nextcloud Installation Phase +# ============================================================================= + +phase_install_nextcloud() { + check_root + + log_info "=== Installing Nextcloud ===" + + wait_for_apt_lock + + log_info "Installing Apache, PHP, MariaDB, and dependencies..." + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + apache2 \ + mariadb-server \ + php \ + php-gd \ + php-json \ + php-mysql \ + php-curl \ + php-mbstring \ + php-intl \ + php-imagick \ + php-xml \ + php-zip \ + php-bz2 \ + php-bcmath \ + php-gmp \ + php-apcu \ + php-redis \ + php-ldap \ + libapache2-mod-php \ + redis-server \ + certbot \ + python3-certbot-apache \ + imagemagick \ + libmagickcore-6.q16-6-extra + + log_success "Packages installed" + + # Configure MariaDB + log_info "Configuring MariaDB..." + + local db_password + db_password=$(generate_password 32) + + mysql -u root </root/.nextcloud_db_password + chmod 600 /root/.nextcloud_db_password + log_success "MariaDB configured" + + # Download Nextcloud + log_info "Downloading Nextcloud..." + + cd /tmp || { log_error "Cannot enter /tmp"; return 1; } + if [[ ! -f nextcloud.zip ]]; then + wget -q --show-progress "https://download.nextcloud.com/server/releases/latest.zip" -O nextcloud.zip >&2 + fi + + rm -rf /var/www/nextcloud + unzip -q nextcloud.zip -d /var/www/ + chown -R www-data:www-data /var/www/nextcloud + + log_success "Nextcloud downloaded and extracted" + + # Configure Apache + log_info "Configuring Apache..." + + cat >/etc/apache2/sites-available/nextcloud.conf <<'EOF' + + ServerAdmin admin@localhost + DocumentRoot /var/www/nextcloud + + + Require all granted + AllowOverride All + Options FollowSymLinks MultiViews + + + Dav off + + + + ErrorLog ${APACHE_LOG_DIR}/nextcloud_error.log + CustomLog ${APACHE_LOG_DIR}/nextcloud_access.log combined + +EOF + + a2enmod rewrite + a2enmod headers + a2enmod env + a2enmod dir + a2enmod mime + a2enmod ssl + a2dissite 000-default + a2ensite nextcloud + + systemctl restart apache2 + + log_success "Apache configured" + + # Configure PHP + log_info "Configuring PHP..." + + local php_version + php_version=$(php -v | head -1 | grep -oP '\d+\.\d+') + + local php_ini="/etc/php/${php_version}/apache2/php.ini" + + sed -i 's/memory_limit = .*/memory_limit = 512M/' "$php_ini" + sed -i 's/upload_max_filesize = .*/upload_max_filesize = 16G/' "$php_ini" + sed -i 's/post_max_size = .*/post_max_size = 16G/' "$php_ini" + sed -i 's/max_execution_time = .*/max_execution_time = 3600/' "$php_ini" + sed -i 's/max_input_time = .*/max_input_time = 3600/' "$php_ini" + sed -i 's/;date.timezone =.*/date.timezone = Europe\/Warsaw/' "$php_ini" + + if ! grep -q "opcache.interned_strings_buffer" "$php_ini"; then + cat >>"$php_ini" <<'EOF' + +; Nextcloud optimizations +opcache.enable=1 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=10000 +opcache.memory_consumption=128 +opcache.save_comments=1 +opcache.revalidate_freq=1 + +; APCu configuration +apc.enable_cli=1 +EOF + fi + + systemctl restart apache2 + + log_success "PHP configured" + + # Configure Redis + log_info "Configuring Redis..." + + systemctl enable redis-server + systemctl start redis-server + + log_success "Redis configured" + + # Install Nextcloud + log_info "Installing Nextcloud..." + + auto_generate_nextcloud_password + + local pi_ip + pi_ip=$(hostname -I | awk '{print $1}') + + cd /var/www/nextcloud || { log_error "Nextcloud is not installed at /var/www/nextcloud"; return 1; } + sudo -u www-data php occ maintenance:install \ + --database "mysql" \ + --database-name "nextcloud" \ + --database-user "nextcloud" \ + --database-pass "$db_password" \ + --admin-user "$NEXTCLOUD_ADMIN_USER" \ + --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \ + --data-dir "$NEXTCLOUD_DATA_DIR" + + # Configure trusted domains + sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_ip" + sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" + sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local" + + # Configure caching + sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu' + sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' + sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis' + sudo -u www-data php occ config:system:set redis host --value='localhost' + sudo -u www-data php occ config:system:set redis port --value=6379 --type=integer + + # Set default phone region + sudo -u www-data php occ config:system:set default_phone_region --value='PL' + + # Set maintenance window + sudo -u www-data php occ config:system:set maintenance_window_start --value=1 --type=integer + + log_success "Nextcloud installed" + + # Setup background jobs + log_info "Setting up Nextcloud background jobs..." + + sudo -u www-data php occ background:cron + + # Add cron job + ( + crontab -u www-data -l 2>/dev/null || true + echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" + ) | sort -u | crontab -u www-data - + + log_success "Cron jobs configured" + + # Verify installation + log_info "Verifying Nextcloud installation..." + + if sudo -u www-data php occ status | grep -q "installed: true"; then + log_success "Nextcloud is responding!" + sudo -u www-data php occ status + else + log_warning "Nextcloud may not be fully configured" + fi + + save_config + + log_success "========================================" + log_success "Nextcloud installation complete!" + log_success "========================================" + log_info "Access Nextcloud at: http://$pi_ip" + log_info "Admin user: $NEXTCLOUD_ADMIN_USER" + log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" + log_info "Database password saved at: /root/.nextcloud_db_password" +} diff --git a/linux_configuration/scripts/single_use/features/lib/rpi_nc_ssl.sh b/linux_configuration/scripts/single_use/features/lib/rpi_nc_ssl.sh new file mode 100755 index 00000000..7c92618c --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/rpi_nc_ssl.sh @@ -0,0 +1,197 @@ +#!/bin/bash +# SSL setup, local and remote. +# +# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ============================================================================= +# Setup Let's Encrypt SSL with DuckDNS +# ============================================================================= + +phase_setup_ssl() { + check_root + + log_info "=== Setting up Let's Encrypt SSL with DuckDNS ===" + + # Check if DuckDNS is configured + if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then + echo + log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." + log_info "1. Go to https://www.duckdns.org/ and sign in with Google/GitHub/etc." + log_info "2. Create a subdomain (e.g., 'myhomecloud' for myhomecloud.duckdns.org)" + log_info "3. Copy your token from the DuckDNS page" + echo + + read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN + read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN + read -r -p "Enter your email (for Let's Encrypt notifications): " LETSENCRYPT_EMAIL + + if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]] || [[ -z $LETSENCRYPT_EMAIL ]]; then + die "All fields are required" + fi + fi + + local full_domain="${DUCKDNS_DOMAIN}.duckdns.org" + local pi_local_ip + pi_local_ip=$(hostname -I | awk '{print $1}') + + # Get public IP for DuckDNS (Let's Encrypt needs external access) + local public_ip + public_ip=$(curl -s https://api.ipify.org) || public_ip=$(curl -s https://ifconfig.me) || true + + log_info "Domain: $full_domain" + log_info "Pi local IP: $pi_local_ip" + log_info "Public IP: $public_ip" + + echo + log_warning "=== IMPORTANT: Port Forwarding Required ===" + log_warning "For Let's Encrypt to work, you MUST forward ports on your router:" + log_warning " - Forward port 80 (HTTP) to $pi_local_ip" + log_warning " - Forward port 443 (HTTPS) to $pi_local_ip" + log_warning "" + log_warning "Go to your router admin page (usually http://192.168.1.1)" + log_warning "and set up port forwarding before continuing." + echo + read -r -p "Have you set up port forwarding? (yes/no): " port_forward_done + + if [[ $port_forward_done != "yes" ]]; then + log_info "Please set up port forwarding and run this command again." + log_info "Without port forwarding, Let's Encrypt cannot verify your domain." + exit 0 + fi + + # Update DuckDNS to point to PUBLIC IP (not local IP) + log_info "Updating DuckDNS to point to public IP $public_ip..." + local duckdns_response + # When ip= is empty, DuckDNS auto-detects the public IP + duckdns_response=$(curl -s "https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=") + + if [[ $duckdns_response != "OK" ]]; then + die "Failed to update DuckDNS: $duckdns_response" + fi + log_success "DuckDNS updated to public IP" + + # Set up automatic DuckDNS updates (cron) - auto-detect public IP + log_info "Setting up automatic DuckDNS IP updates..." + mkdir -p /opt/duckdns + cat >/opt/duckdns/duck.sh </dev/null || true) | grep -v "duckdns" | { + cat + echo "*/5 * * * * /opt/duckdns/duck.sh >/dev/null 2>&1" + } | crontab - + + log_success "DuckDNS auto-update configured" + + # Wait for DNS propagation + log_info "Waiting for DNS propagation (this may take a minute)..." + local dns_ip="" + local attempts=0 + while [[ $dns_ip != "$public_ip" ]] && [[ $attempts -lt 12 ]]; do + sleep 5 + dns_ip=$(dig +short "$full_domain" 2>/dev/null | tail -1) || true + attempts=$((attempts + 1)) + log_info " DNS lookup: $dns_ip (expecting $public_ip, attempt $attempts/12)" + done + + if [[ $dns_ip != "$public_ip" ]]; then + log_warning "DNS may not have propagated yet. Continuing anyway..." + else + log_success "DNS verified: $full_domain -> $public_ip" + fi + + # Install certbot if not present + if ! command -v certbot &>/dev/null; then + log_info "Installing certbot..." + DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-apache + fi + + # Get Let's Encrypt certificate + log_info "Obtaining Let's Encrypt certificate..." + + # First update Apache config with the new domain + cat >/etc/apache2/sites-available/nextcloud-ssl.conf < + ServerAdmin ${LETSENCRYPT_EMAIL} + DocumentRoot /var/www/nextcloud + ServerName ${full_domain} + + SSLEngine on + # Certbot will update these paths + SSLCertificateFile /etc/ssl/nextcloud/server.crt + SSLCertificateKeyFile /etc/ssl/nextcloud/server.key + + + Require all granted + AllowOverride All + Options FollowSymLinks MultiViews + + + Dav off + + + + # Security headers + Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains" + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "SAMEORIGIN" + Header always set X-XSS-Protection "1; mode=block" + Header always set Referrer-Policy "no-referrer" + + ErrorLog \${APACHE_LOG_DIR}/nextcloud_ssl_error.log + CustomLog \${APACHE_LOG_DIR}/nextcloud_ssl_access.log combined + + + + ServerName ${full_domain} + Redirect permanent / https://${full_domain}/ + +EOF + + systemctl reload apache2 + + # Run certbot + certbot --apache -d "$full_domain" --non-interactive --agree-tos --email "$LETSENCRYPT_EMAIL" --redirect + + log_success "Let's Encrypt certificate obtained!" + + # Update Nextcloud trusted domains + log_info "Updating Nextcloud configuration..." + cd /var/www/nextcloud || { log_error "Nextcloud is not installed at /var/www/nextcloud"; return 1; } + sudo -u www-data php occ config:system:set trusted_domains 0 --value="$full_domain" + sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$full_domain" + sudo -u www-data php occ config:system:set overwriteprotocol --value="https" + sudo -u www-data php occ config:system:set overwritehost --value="$full_domain" + + # Keep local access working + sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_local_ip" + sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" + sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local" + + log_success "Nextcloud configured for $full_domain" + + # Set up auto-renewal + log_info "Setting up automatic certificate renewal..." + systemctl enable certbot.timer + systemctl start certbot.timer + + log_success "========================================" + log_success "Let's Encrypt SSL configured!" + log_success "========================================" + echo + log_info "Your Nextcloud is now accessible at:" + log_info " https://$full_domain (from anywhere on the internet)" + log_info " https://$pi_local_ip (from your local network)" + echo + log_info "This certificate is trusted by ALL browsers and devices automatically!" + log_info "No manual certificate installation required." + echo + log_info "Certificate auto-renewal is enabled." + log_info "DuckDNS IP auto-update is enabled." +} diff --git a/linux_configuration/scripts/single_use/features/lib/rpi_nc_ssl_remote.sh b/linux_configuration/scripts/single_use/features/lib/rpi_nc_ssl_remote.sh new file mode 100755 index 00000000..76d47cfc --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/rpi_nc_ssl_remote.sh @@ -0,0 +1,111 @@ +#!/bin/bash +# Remote SSL setup and the remote install phase. +# +# Sourced by raspberry_pi_nextcloud.sh; split out to keep rpi_nc_ssl.sh +# under the 250-line cap. Sourced rather than run, so it inherits the +# caller's strict mode and the variables defined above the source line. + +phase_setup_ssl_remote() { + log_info "=== Setting up Let's Encrypt SSL via SSH ===" + + if [[ -z $PI_PASSWORD ]]; then + die "PI_PASSWORD not set. Run install-remote first." + fi + + local pi_ip + pi_ip=$(discover_raspberry_pi) + + if [[ -z $pi_ip ]]; then + die "Failed to discover Raspberry Pi" + fi + + # Get DuckDNS credentials if not set + if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then + echo + log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." + log_info "1. Go to https://www.duckdns.org/ and sign in" + log_info "2. Create a subdomain (e.g., 'myhomecloud')" + log_info "3. Copy your token" + echo + + read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN + read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN + read -r -p "Enter your email (for Let's Encrypt): " LETSENCRYPT_EMAIL + fi + + save_config + + log_info "Copying script to Pi..." + sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh" + + log_info "Running SSL setup on Pi..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S DUCKDNS_DOMAIN='$DUCKDNS_DOMAIN' DUCKDNS_TOKEN='$DUCKDNS_TOKEN' LETSENCRYPT_EMAIL='$LETSENCRYPT_EMAIL' bash /tmp/raspberry_pi_nextcloud.sh setup-ssl" + + local full_domain="${DUCKDNS_DOMAIN}.duckdns.org" + + log_success "========================================" + log_success "SSL setup complete!" + log_success "========================================" + echo + log_info "Access your Nextcloud at: https://$full_domain" + log_info "This works on ALL devices without certificate warnings!" +} + +# ============================================================================= +# Remote Installation +# ============================================================================= + +phase_install_remote() { + log_info "=== Installing Nextcloud via SSH ===" + + if [[ -z $PI_PASSWORD ]]; then + die "PI_PASSWORD not set. Did you run flash script first?" + fi + + local pi_ip + pi_ip=$(discover_raspberry_pi) + + if [[ -z $pi_ip ]]; then + die "Failed to discover Raspberry Pi" + fi + + log_info "Using Raspberry Pi at: $pi_ip" + + # Remove old host key if present + ssh-keygen -R "$pi_ip" 2>/dev/null || true + + log_info "Copying script to Pi..." + sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh" + + log_info "Running system configuration on Pi..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh configure" + + log_info "Installing Nextcloud on Pi..." + auto_generate_nextcloud_password + save_config + + log_success "Nextcloud admin user: $NEXTCLOUD_ADMIN_USER" + log_success "Nextcloud admin password: $NEXTCLOUD_ADMIN_PASSWORD" + + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S NEXTCLOUD_ADMIN_PASSWORD='$NEXTCLOUD_ADMIN_PASSWORD' NEXTCLOUD_ADMIN_USER='$NEXTCLOUD_ADMIN_USER' bash /tmp/raspberry_pi_nextcloud.sh install-local" + + log_info "Fixing Nextcloud issues..." + sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ + "echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh fix" + + log_success "========================================" + log_success "Remote Nextcloud installation complete!" + log_success "========================================" + echo + log_info "=== Access Information ===" + log_info "Nextcloud URL: https://$pi_ip" + log_info "Admin user: $NEXTCLOUD_ADMIN_USER" + log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" + log_info "All credentials saved in: $CONFIG_FILE" + echo + log_info "=== Trust the certificate ===" + log_info "Run: $0 install-ca" +} diff --git a/linux_configuration/scripts/single_use/features/lib/rpi_nc_system.sh b/linux_configuration/scripts/single_use/features/lib/rpi_nc_system.sh new file mode 100755 index 00000000..0e9e2fd1 --- /dev/null +++ b/linux_configuration/scripts/single_use/features/lib/rpi_nc_system.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# System configuration phase. +# +# Sourced by raspberry_pi_nextcloud.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +# ============================================================================= +# System Configuration Phase +# ============================================================================= + +phase_configure_system() { + check_root + + log_info "=== Configuring Raspberry Pi System ===" + + wait_for_apt_lock + + log_info "Fixing any broken packages..." + DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confdef --force-confold || true + + log_info "Updating system packages..." + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y + + log_info "Setting timezone to $PI_TIMEZONE..." + timedatectl set-timezone "$PI_TIMEZONE" + + log_info "Configuring locale..." + sed -i "s/^# *$PI_LOCALE/$PI_LOCALE/" /etc/locale.gen + locale-gen + update-locale LANG="$PI_LOCALE" + + log_info "Hardening SSH configuration..." + cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup + + cat >>/etc/ssh/sshd_config.d/hardening.conf <<'EOF' +# Security hardening +PermitRootLogin no +PasswordAuthentication yes +PubkeyAuthentication yes +X11Forwarding no +MaxAuthTries 3 +ClientAliveInterval 300 +ClientAliveCountMax 2 +EOF + + systemctl restart sshd + + log_info "Installing useful packages..." + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + vim \ + htop \ + curl \ + wget \ + git \ + ufw \ + fail2ban \ + unattended-upgrades + + log_info "Configuring firewall..." + ufw default deny incoming + ufw default allow outgoing + ufw allow ssh + ufw allow 80/tcp + ufw allow 443/tcp + ufw --force enable + + log_info "Configuring fail2ban..." + cat >/etc/fail2ban/jail.local <<'EOF' +[DEFAULT] +bantime = 1h +findtime = 10m +maxretry = 5 + +[sshd] +enabled = true +port = ssh +filter = sshd +logpath = /var/log/auth.log +maxretry = 3 +EOF + + systemctl enable fail2ban + systemctl restart fail2ban + + log_info "Enabling automatic security updates..." + cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF' +Unattended-Upgrade::Origins-Pattern { + "origin=Debian,codename=${distro_codename},label=Debian-Security"; + "origin=Raspbian,codename=${distro_codename},label=Raspbian"; +}; +Unattended-Upgrade::AutoFixInterruptedDpkg "true"; +Unattended-Upgrade::Remove-Unused-Dependencies "true"; +EOF + + cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF' +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; +APT::Periodic::AutocleanInterval "7"; +EOF + + log_success "System configuration complete!" +} diff --git a/linux_configuration/scripts/single_use/features/raspberry_pi_nextcloud.sh b/linux_configuration/scripts/single_use/features/raspberry_pi_nextcloud.sh index bf5d62a3..66b8f043 100755 --- a/linux_configuration/scripts/single_use/features/raspberry_pi_nextcloud.sh +++ b/linux_configuration/scripts/single_use/features/raspberry_pi_nextcloud.sh @@ -108,1134 +108,21 @@ auto_generate_nextcloud_password() { fi } -wait_for_apt_lock() { - local max_wait=600 - local waited=0 +# shellcheck source=lib/rpi_nc_deps.sh +source "$SCRIPT_DIR/lib/rpi_nc_deps.sh" +# shellcheck source=lib/rpi_nc_system.sh +source "$SCRIPT_DIR/lib/rpi_nc_system.sh" +# shellcheck source=lib/rpi_nc_install.sh +source "$SCRIPT_DIR/lib/rpi_nc_install.sh" +# shellcheck source=lib/rpi_nc_fixes.sh +source "$SCRIPT_DIR/lib/rpi_nc_fixes.sh" +# shellcheck source=lib/rpi_nc_ssl.sh +source "$SCRIPT_DIR/lib/rpi_nc_ssl.sh" +# shellcheck source=lib/rpi_nc_ssl_remote.sh +source "$SCRIPT_DIR/lib/rpi_nc_ssl_remote.sh" +# shellcheck source=lib/rpi_nc_ca.sh +source "$SCRIPT_DIR/lib/rpi_nc_ca.sh" - while fuser /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock >/dev/null 2>&1; do - if [[ $waited -eq 0 ]]; then - log_info "Waiting for other apt/dpkg processes to finish..." - pgrep -a 'apt|dpkg' | head -5 >&2 || true - fi - sleep 5 - waited=$((waited + 5)) - if [[ $waited -ge $max_wait ]]; then - die "Timeout waiting for apt lock after ${max_wait}s" - fi - if [[ $((waited % 30)) -eq 0 ]]; then - log_info "Still waiting... (${waited}s elapsed)" - fi - done - - if [[ $waited -gt 0 ]]; then - log_success "Apt lock acquired after ${waited}s" - fi -} - -# ============================================================================= -# Network Discovery (for remote installation) -# ============================================================================= - -ensure_dependencies() { - local missing_packages=() - - if ! command -v nmap &>/dev/null; then - missing_packages+=("nmap") - fi - - if ! command -v sshpass &>/dev/null; then - missing_packages+=("sshpass") - fi - - if [[ ${#missing_packages[@]} -gt 0 ]]; then - log_info "Installing missing packages: ${missing_packages[*]}" - - if command -v pacman &>/dev/null; then - sudo pacman -S --noconfirm "${missing_packages[@]}" - elif command -v apt-get &>/dev/null; then - sudo apt-get update && sudo apt-get install -y "${missing_packages[@]}" - elif command -v dnf &>/dev/null; then - sudo dnf install -y "${missing_packages[@]}" - else - die "Could not detect package manager. Please install manually: ${missing_packages[*]}" - fi - fi -} - -discover_raspberry_pi() { - log_info "Auto-discovering Raspberry Pi on local network..." - - ensure_dependencies - - local my_ip - my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1) - local gateway - gateway=$(ip route | grep default | awk '{print $3}' | head -1) - local network="${gateway%.*}.0/24" - - log_info "Local IP: $my_ip, Network: $network" - log_info "Scanning for Raspberry Pi (hostname: $PI_HOSTNAME)..." - - local pi_ip="" - - # Try resolving hostname directly - pi_ip=$(getent hosts "$PI_HOSTNAME" 2>/dev/null | awk '{print $1}' | head -1) || true - if [[ -z $pi_ip ]]; then - pi_ip=$(getent hosts "${PI_HOSTNAME}.local" 2>/dev/null | awk '{print $1}' | head -1) || true - fi - - if [[ -n $pi_ip ]]; then - log_success "Found Pi by hostname: $pi_ip" - echo "$pi_ip" - return - fi - - log_info "Hostname resolution failed, scanning network..." - nmap -sn -T4 "$network" &>/dev/null || true - - local ssh_hosts - ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2>/dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u) || true - - if [[ -z $ssh_hosts ]]; then - die "No SSH-enabled devices found. Is the Pi connected and booted?" - fi - - log_info "Found SSH-enabled devices: $(echo "$ssh_hosts" | tr '\n' ' ')" - - for ip in $ssh_hosts; do - log_info "Trying $ip with user '$PI_USER'..." - - if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "hostname" 2>/dev/null | grep -qi "$PI_HOSTNAME"; then - log_success "Found Raspberry Pi at $ip" - echo "$ip" - return - fi - - if sshpass -p "$PI_PASSWORD" ssh -o BatchMode=no -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${PI_USER}@${ip}" "echo ok" 2>/dev/null | grep -q "ok"; then - log_success "Found device responding to Pi credentials at $ip" - echo "$ip" - return - fi - done - - die "Could not find Raspberry Pi on network." -} - -# ============================================================================= -# System Configuration Phase -# ============================================================================= - -phase_configure_system() { - check_root - - log_info "=== Configuring Raspberry Pi System ===" - - wait_for_apt_lock - - log_info "Fixing any broken packages..." - DEBIAN_FRONTEND=noninteractive dpkg --configure -a --force-confdef --force-confold || true - - log_info "Updating system packages..." - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade -y - - log_info "Setting timezone to $PI_TIMEZONE..." - timedatectl set-timezone "$PI_TIMEZONE" - - log_info "Configuring locale..." - sed -i "s/^# *$PI_LOCALE/$PI_LOCALE/" /etc/locale.gen - locale-gen - update-locale LANG="$PI_LOCALE" - - log_info "Hardening SSH configuration..." - cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup - - cat >>/etc/ssh/sshd_config.d/hardening.conf <<'EOF' -# Security hardening -PermitRootLogin no -PasswordAuthentication yes -PubkeyAuthentication yes -X11Forwarding no -MaxAuthTries 3 -ClientAliveInterval 300 -ClientAliveCountMax 2 -EOF - - systemctl restart sshd - - log_info "Installing useful packages..." - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - vim \ - htop \ - curl \ - wget \ - git \ - ufw \ - fail2ban \ - unattended-upgrades - - log_info "Configuring firewall..." - ufw default deny incoming - ufw default allow outgoing - ufw allow ssh - ufw allow 80/tcp - ufw allow 443/tcp - ufw --force enable - - log_info "Configuring fail2ban..." - cat >/etc/fail2ban/jail.local <<'EOF' -[DEFAULT] -bantime = 1h -findtime = 10m -maxretry = 5 - -[sshd] -enabled = true -port = ssh -filter = sshd -logpath = /var/log/auth.log -maxretry = 3 -EOF - - systemctl enable fail2ban - systemctl restart fail2ban - - log_info "Enabling automatic security updates..." - cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF' -Unattended-Upgrade::Origins-Pattern { - "origin=Debian,codename=${distro_codename},label=Debian-Security"; - "origin=Raspbian,codename=${distro_codename},label=Raspbian"; -}; -Unattended-Upgrade::AutoFixInterruptedDpkg "true"; -Unattended-Upgrade::Remove-Unused-Dependencies "true"; -EOF - - cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF' -APT::Periodic::Update-Package-Lists "1"; -APT::Periodic::Unattended-Upgrade "1"; -APT::Periodic::AutocleanInterval "7"; -EOF - - log_success "System configuration complete!" -} - -# ============================================================================= -# Nextcloud Installation Phase -# ============================================================================= - -phase_install_nextcloud() { - check_root - - log_info "=== Installing Nextcloud ===" - - wait_for_apt_lock - - log_info "Installing Apache, PHP, MariaDB, and dependencies..." - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - apache2 \ - mariadb-server \ - php \ - php-gd \ - php-json \ - php-mysql \ - php-curl \ - php-mbstring \ - php-intl \ - php-imagick \ - php-xml \ - php-zip \ - php-bz2 \ - php-bcmath \ - php-gmp \ - php-apcu \ - php-redis \ - php-ldap \ - libapache2-mod-php \ - redis-server \ - certbot \ - python3-certbot-apache \ - imagemagick \ - libmagickcore-6.q16-6-extra - - log_success "Packages installed" - - # Configure MariaDB - log_info "Configuring MariaDB..." - - local db_password - db_password=$(generate_password 32) - - mysql -u root </root/.nextcloud_db_password - chmod 600 /root/.nextcloud_db_password - log_success "MariaDB configured" - - # Download Nextcloud - log_info "Downloading Nextcloud..." - - cd /tmp - if [[ ! -f nextcloud.zip ]]; then - wget -q --show-progress "https://download.nextcloud.com/server/releases/latest.zip" -O nextcloud.zip >&2 - fi - - rm -rf /var/www/nextcloud - unzip -q nextcloud.zip -d /var/www/ - chown -R www-data:www-data /var/www/nextcloud - - log_success "Nextcloud downloaded and extracted" - - # Configure Apache - log_info "Configuring Apache..." - - cat >/etc/apache2/sites-available/nextcloud.conf <<'EOF' - - ServerAdmin admin@localhost - DocumentRoot /var/www/nextcloud - - - Require all granted - AllowOverride All - Options FollowSymLinks MultiViews - - - Dav off - - - - ErrorLog ${APACHE_LOG_DIR}/nextcloud_error.log - CustomLog ${APACHE_LOG_DIR}/nextcloud_access.log combined - -EOF - - a2enmod rewrite - a2enmod headers - a2enmod env - a2enmod dir - a2enmod mime - a2enmod ssl - a2dissite 000-default - a2ensite nextcloud - - systemctl restart apache2 - - log_success "Apache configured" - - # Configure PHP - log_info "Configuring PHP..." - - local php_version - php_version=$(php -v | head -1 | grep -oP '\d+\.\d+') - - local php_ini="/etc/php/${php_version}/apache2/php.ini" - - sed -i 's/memory_limit = .*/memory_limit = 512M/' "$php_ini" - sed -i 's/upload_max_filesize = .*/upload_max_filesize = 16G/' "$php_ini" - sed -i 's/post_max_size = .*/post_max_size = 16G/' "$php_ini" - sed -i 's/max_execution_time = .*/max_execution_time = 3600/' "$php_ini" - sed -i 's/max_input_time = .*/max_input_time = 3600/' "$php_ini" - sed -i 's/;date.timezone =.*/date.timezone = Europe\/Warsaw/' "$php_ini" - - if ! grep -q "opcache.interned_strings_buffer" "$php_ini"; then - cat >>"$php_ini" <<'EOF' - -; Nextcloud optimizations -opcache.enable=1 -opcache.interned_strings_buffer=16 -opcache.max_accelerated_files=10000 -opcache.memory_consumption=128 -opcache.save_comments=1 -opcache.revalidate_freq=1 - -; APCu configuration -apc.enable_cli=1 -EOF - fi - - systemctl restart apache2 - - log_success "PHP configured" - - # Configure Redis - log_info "Configuring Redis..." - - systemctl enable redis-server - systemctl start redis-server - - log_success "Redis configured" - - # Install Nextcloud - log_info "Installing Nextcloud..." - - auto_generate_nextcloud_password - - local pi_ip - pi_ip=$(hostname -I | awk '{print $1}') - - cd /var/www/nextcloud - sudo -u www-data php occ maintenance:install \ - --database "mysql" \ - --database-name "nextcloud" \ - --database-user "nextcloud" \ - --database-pass "$db_password" \ - --admin-user "$NEXTCLOUD_ADMIN_USER" \ - --admin-pass "$NEXTCLOUD_ADMIN_PASSWORD" \ - --data-dir "$NEXTCLOUD_DATA_DIR" - - # Configure trusted domains - sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_ip" - sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" - sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local" - - # Configure caching - sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu' - sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis' - sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis' - sudo -u www-data php occ config:system:set redis host --value='localhost' - sudo -u www-data php occ config:system:set redis port --value=6379 --type=integer - - # Set default phone region - sudo -u www-data php occ config:system:set default_phone_region --value='PL' - - # Set maintenance window - sudo -u www-data php occ config:system:set maintenance_window_start --value=1 --type=integer - - log_success "Nextcloud installed" - - # Setup background jobs - log_info "Setting up Nextcloud background jobs..." - - sudo -u www-data php occ background:cron - - # Add cron job - ( - crontab -u www-data -l 2>/dev/null || true - echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" - ) | sort -u | crontab -u www-data - - - log_success "Cron jobs configured" - - # Verify installation - log_info "Verifying Nextcloud installation..." - - if sudo -u www-data php occ status | grep -q "installed: true"; then - log_success "Nextcloud is responding!" - sudo -u www-data php occ status - else - log_warning "Nextcloud may not be fully configured" - fi - - save_config - - log_success "========================================" - log_success "Nextcloud installation complete!" - log_success "========================================" - log_info "Access Nextcloud at: http://$pi_ip" - log_info "Admin user: $NEXTCLOUD_ADMIN_USER" - log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" - log_info "Database password saved at: /root/.nextcloud_db_password" -} - -# ============================================================================= -# Fix Nextcloud Issues -# ============================================================================= - -# shellcheck disable=SC2120 # Function does not use positional args -phase_fix_issues() { - check_root - - log_info "=== Fixing Nextcloud Issues ===" - - cd /var/www/nextcloud - - # 1. Fix background jobs (cron not running properly) - log_info "Fixing background jobs..." - - # Ensure cron is set as background job method - sudo -u www-data php occ background:cron - - # Ensure cron job exists and is correct - ( - crontab -u www-data -l 2>/dev/null | grep -v "cron.php" - echo "*/5 * * * * php -f /var/www/nextcloud/cron.php" - ) | crontab -u www-data - - - # Run cron manually now to reset the timer - log_info "Running cron job manually..." - sudo -u www-data php /var/www/nextcloud/cron.php - - log_success "Background jobs configured" - - # 2. Setup HTTPS with proper CA-signed certificate - log_info "Setting up HTTPS with trusted CA..." - - local pi_ip - pi_ip=$(hostname -I | awk '{print $1}') - - local ssl_dir="/etc/ssl/nextcloud" - mkdir -p "$ssl_dir" - chmod 700 "$ssl_dir" - - # Generate CA if it doesn't exist - if [[ ! -f "$ssl_dir/ca.crt" ]]; then - log_info "Creating Certificate Authority (CA)..." - - # Generate CA private key - openssl genrsa -out "$ssl_dir/ca.key" 4096 - chmod 600 "$ssl_dir/ca.key" - - # Generate CA certificate (valid for 10 years) - openssl req -x509 -new -nodes -key "$ssl_dir/ca.key" \ - -sha256 -days 3650 \ - -out "$ssl_dir/ca.crt" \ - -subj "/C=PL/ST=Home/L=Local/O=Nextcloud Home CA/OU=Certificate Authority/CN=Nextcloud Home CA" - - log_success "CA created: $ssl_dir/ca.crt" - fi - - # Generate server certificate signed by our CA - local regenerate="${1:-}" - if [[ ! -f "$ssl_dir/server.crt" ]] || [[ $regenerate == "--regenerate" ]]; then - log_info "Generating server certificate signed by CA..." - - # Generate server private key - openssl genrsa -out "$ssl_dir/server.key" 2048 - chmod 600 "$ssl_dir/server.key" - - # Create certificate signing request (CSR) - openssl req -new -key "$ssl_dir/server.key" \ - -out "$ssl_dir/server.csr" \ - -subj "/C=PL/ST=Home/L=Local/O=Nextcloud/OU=Server/CN=$PI_HOSTNAME" - - # Create extension file for SAN (Subject Alternative Names) - # This allows the certificate to be valid for hostname, IP, and .local - cat >"$ssl_dir/server.ext" </etc/apache2/sites-available/nextcloud-ssl.conf < - ServerAdmin admin@localhost - DocumentRoot /var/www/nextcloud - ServerName $PI_HOSTNAME - ServerAlias ${PI_HOSTNAME}.local $pi_ip - - SSLEngine on - SSLCertificateFile $ssl_dir/server.crt - SSLCertificateKeyFile $ssl_dir/server.key - - - Require all granted - AllowOverride All - Options FollowSymLinks MultiViews - - - Dav off - - - - # Security headers - Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains" - Header always set X-Content-Type-Options "nosniff" - Header always set X-Frame-Options "SAMEORIGIN" - Header always set X-XSS-Protection "1; mode=block" - Header always set Referrer-Policy "no-referrer" - - ErrorLog \${APACHE_LOG_DIR}/nextcloud_ssl_error.log - CustomLog \${APACHE_LOG_DIR}/nextcloud_ssl_access.log combined - - -# Redirect HTTP to HTTPS - - ServerName $PI_HOSTNAME - ServerAlias $pi_ip - Redirect permanent / https://$PI_HOSTNAME/ - -EOF - - a2enmod ssl - a2enmod headers - a2ensite nextcloud-ssl - - # Update Nextcloud config for HTTPS - sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$PI_HOSTNAME" - sudo -u www-data php occ config:system:set overwriteprotocol --value="https" - - systemctl restart apache2 - - log_success "HTTPS configured with CA-signed certificate" - - # 3. Run mimetype migrations - log_info "Running mimetype migrations..." - sudo -u www-data php occ maintenance:repair --include-expensive - log_success "Mimetype migrations complete" - - # 4. Add missing database indices - log_info "Adding missing database indices..." - sudo -u www-data php occ db:add-missing-indices - log_success "Database indices added" - - # 5. Install ImageMagick SVG support - log_info "Installing ImageMagick SVG support..." - DEBIAN_FRONTEND=noninteractive apt-get install -y libmagickcore-6.q16-6-extra - - # Enable SVG in ImageMagick policy - local policy_file="/etc/ImageMagick-6/policy.xml" - if [[ -f $policy_file ]]; then - # Remove SVG restrictions if present - sed -i 's///' "$policy_file" - # If no SVG policy exists, add one allowing it - if ! grep -q 'pattern="SVG"' "$policy_file"; then - sed -i '//a\ ' "$policy_file" - fi - fi - - systemctl restart apache2 - log_success "ImageMagick SVG support configured" - - # 6. Set up basic SMTP (placeholder - user needs to configure actual mail server) - log_info "Note: Email server not configured - please configure in Nextcloud admin settings" - - # 7. Clear any remaining warnings - log_info "Clearing Nextcloud caches..." - sudo -u www-data php occ maintenance:repair - sudo -u www-data php occ files:scan --all - - # 8. Verify all fixes - log_info "Verifying fixes..." - - # Run cron again to update last run time - sudo -u www-data php /var/www/nextcloud/cron.php - - log_success "========================================" - log_success "Nextcloud issues fixed!" - log_success "========================================" - echo - log_info "Summary of changes:" - log_info " ✓ Background jobs (cron) configured and running" - log_info " ✓ HTTPS enabled with CA-signed certificate" - log_info " ✓ Strict-Transport-Security header added" - log_info " ✓ Mimetype migrations completed" - log_info " ✓ Missing database indices added" - log_info " ✓ ImageMagick SVG support installed" - echo - log_info "Current certificate: self-signed CA (requires manual install on devices)" - log_info " - Run: $0 install-ca (on your laptop)" - log_info " - Or download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" - echo - log_info "For auto-trusted HTTPS on ALL devices (recommended):" - log_info " 1. Get free domain at https://www.duckdns.org/" - log_info " 2. Run: $0 setup-ssl" - echo - log_info "Access Nextcloud at: https://$PI_HOSTNAME" -} - -# ============================================================================= -# Setup Let's Encrypt SSL with DuckDNS -# ============================================================================= - -phase_setup_ssl() { - check_root - - log_info "=== Setting up Let's Encrypt SSL with DuckDNS ===" - - # Check if DuckDNS is configured - if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then - echo - log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." - log_info "1. Go to https://www.duckdns.org/ and sign in with Google/GitHub/etc." - log_info "2. Create a subdomain (e.g., 'myhomecloud' for myhomecloud.duckdns.org)" - log_info "3. Copy your token from the DuckDNS page" - echo - - read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN - read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN - read -r -p "Enter your email (for Let's Encrypt notifications): " LETSENCRYPT_EMAIL - - if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]] || [[ -z $LETSENCRYPT_EMAIL ]]; then - die "All fields are required" - fi - fi - - local full_domain="${DUCKDNS_DOMAIN}.duckdns.org" - local pi_local_ip - pi_local_ip=$(hostname -I | awk '{print $1}') - - # Get public IP for DuckDNS (Let's Encrypt needs external access) - local public_ip - public_ip=$(curl -s https://api.ipify.org) || public_ip=$(curl -s https://ifconfig.me) || true - - log_info "Domain: $full_domain" - log_info "Pi local IP: $pi_local_ip" - log_info "Public IP: $public_ip" - - echo - log_warning "=== IMPORTANT: Port Forwarding Required ===" - log_warning "For Let's Encrypt to work, you MUST forward ports on your router:" - log_warning " - Forward port 80 (HTTP) to $pi_local_ip" - log_warning " - Forward port 443 (HTTPS) to $pi_local_ip" - log_warning "" - log_warning "Go to your router admin page (usually http://192.168.1.1)" - log_warning "and set up port forwarding before continuing." - echo - read -r -p "Have you set up port forwarding? (yes/no): " port_forward_done - - if [[ $port_forward_done != "yes" ]]; then - log_info "Please set up port forwarding and run this command again." - log_info "Without port forwarding, Let's Encrypt cannot verify your domain." - exit 0 - fi - - # Update DuckDNS to point to PUBLIC IP (not local IP) - log_info "Updating DuckDNS to point to public IP $public_ip..." - local duckdns_response - # When ip= is empty, DuckDNS auto-detects the public IP - duckdns_response=$(curl -s "https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=") - - if [[ $duckdns_response != "OK" ]]; then - die "Failed to update DuckDNS: $duckdns_response" - fi - log_success "DuckDNS updated to public IP" - - # Set up automatic DuckDNS updates (cron) - auto-detect public IP - log_info "Setting up automatic DuckDNS IP updates..." - mkdir -p /opt/duckdns - cat >/opt/duckdns/duck.sh </dev/null || true) | grep -v "duckdns" | { - cat - echo "*/5 * * * * /opt/duckdns/duck.sh >/dev/null 2>&1" - } | crontab - - - log_success "DuckDNS auto-update configured" - - # Wait for DNS propagation - log_info "Waiting for DNS propagation (this may take a minute)..." - local dns_ip="" - local attempts=0 - while [[ $dns_ip != "$public_ip" ]] && [[ $attempts -lt 12 ]]; do - sleep 5 - dns_ip=$(dig +short "$full_domain" 2>/dev/null | tail -1) || true - attempts=$((attempts + 1)) - log_info " DNS lookup: $dns_ip (expecting $public_ip, attempt $attempts/12)" - done - - if [[ $dns_ip != "$public_ip" ]]; then - log_warning "DNS may not have propagated yet. Continuing anyway..." - else - log_success "DNS verified: $full_domain -> $public_ip" - fi - - # Install certbot if not present - if ! command -v certbot &>/dev/null; then - log_info "Installing certbot..." - DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-apache - fi - - # Get Let's Encrypt certificate - log_info "Obtaining Let's Encrypt certificate..." - - # First update Apache config with the new domain - cat >/etc/apache2/sites-available/nextcloud-ssl.conf < - ServerAdmin ${LETSENCRYPT_EMAIL} - DocumentRoot /var/www/nextcloud - ServerName ${full_domain} - - SSLEngine on - # Certbot will update these paths - SSLCertificateFile /etc/ssl/nextcloud/server.crt - SSLCertificateKeyFile /etc/ssl/nextcloud/server.key - - - Require all granted - AllowOverride All - Options FollowSymLinks MultiViews - - - Dav off - - - - # Security headers - Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains" - Header always set X-Content-Type-Options "nosniff" - Header always set X-Frame-Options "SAMEORIGIN" - Header always set X-XSS-Protection "1; mode=block" - Header always set Referrer-Policy "no-referrer" - - ErrorLog \${APACHE_LOG_DIR}/nextcloud_ssl_error.log - CustomLog \${APACHE_LOG_DIR}/nextcloud_ssl_access.log combined - - - - ServerName ${full_domain} - Redirect permanent / https://${full_domain}/ - -EOF - - systemctl reload apache2 - - # Run certbot - certbot --apache -d "$full_domain" --non-interactive --agree-tos --email "$LETSENCRYPT_EMAIL" --redirect - - log_success "Let's Encrypt certificate obtained!" - - # Update Nextcloud trusted domains - log_info "Updating Nextcloud configuration..." - cd /var/www/nextcloud - sudo -u www-data php occ config:system:set trusted_domains 0 --value="$full_domain" - sudo -u www-data php occ config:system:set overwrite.cli.url --value="https://$full_domain" - sudo -u www-data php occ config:system:set overwriteprotocol --value="https" - sudo -u www-data php occ config:system:set overwritehost --value="$full_domain" - - # Keep local access working - sudo -u www-data php occ config:system:set trusted_domains 1 --value="$pi_local_ip" - sudo -u www-data php occ config:system:set trusted_domains 2 --value="$PI_HOSTNAME" - sudo -u www-data php occ config:system:set trusted_domains 3 --value="${PI_HOSTNAME}.local" - - log_success "Nextcloud configured for $full_domain" - - # Set up auto-renewal - log_info "Setting up automatic certificate renewal..." - systemctl enable certbot.timer - systemctl start certbot.timer - - log_success "========================================" - log_success "Let's Encrypt SSL configured!" - log_success "========================================" - echo - log_info "Your Nextcloud is now accessible at:" - log_info " https://$full_domain (from anywhere on the internet)" - log_info " https://$pi_local_ip (from your local network)" - echo - log_info "This certificate is trusted by ALL browsers and devices automatically!" - log_info "No manual certificate installation required." - echo - log_info "Certificate auto-renewal is enabled." - log_info "DuckDNS IP auto-update is enabled." -} - -phase_setup_ssl_remote() { - log_info "=== Setting up Let's Encrypt SSL via SSH ===" - - if [[ -z $PI_PASSWORD ]]; then - die "PI_PASSWORD not set. Run install-remote first." - fi - - local pi_ip - pi_ip=$(discover_raspberry_pi) - - if [[ -z $pi_ip ]]; then - die "Failed to discover Raspberry Pi" - fi - - # Get DuckDNS credentials if not set - if [[ -z $DUCKDNS_DOMAIN ]] || [[ -z $DUCKDNS_TOKEN ]]; then - echo - log_info "To get auto-trusted HTTPS, you need a free DuckDNS domain." - log_info "1. Go to https://www.duckdns.org/ and sign in" - log_info "2. Create a subdomain (e.g., 'myhomecloud')" - log_info "3. Copy your token" - echo - - read -r -p "Enter your DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN - read -r -p "Enter your DuckDNS token: " DUCKDNS_TOKEN - read -r -p "Enter your email (for Let's Encrypt): " LETSENCRYPT_EMAIL - fi - - save_config - - log_info "Copying script to Pi..." - sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh" - - log_info "Running SSL setup on Pi..." - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S DUCKDNS_DOMAIN='$DUCKDNS_DOMAIN' DUCKDNS_TOKEN='$DUCKDNS_TOKEN' LETSENCRYPT_EMAIL='$LETSENCRYPT_EMAIL' bash /tmp/raspberry_pi_nextcloud.sh setup-ssl" - - local full_domain="${DUCKDNS_DOMAIN}.duckdns.org" - - log_success "========================================" - log_success "SSL setup complete!" - log_success "========================================" - echo - log_info "Access your Nextcloud at: https://$full_domain" - log_info "This works on ALL devices without certificate warnings!" -} - -# ============================================================================= -# Remote Installation -# ============================================================================= - -phase_install_remote() { - log_info "=== Installing Nextcloud via SSH ===" - - if [[ -z $PI_PASSWORD ]]; then - die "PI_PASSWORD not set. Did you run flash script first?" - fi - - local pi_ip - pi_ip=$(discover_raspberry_pi) - - if [[ -z $pi_ip ]]; then - die "Failed to discover Raspberry Pi" - fi - - log_info "Using Raspberry Pi at: $pi_ip" - - # Remove old host key if present - ssh-keygen -R "$pi_ip" 2>/dev/null || true - - log_info "Copying script to Pi..." - sshpass -p "$PI_PASSWORD" scp -o StrictHostKeyChecking=no "$0" "${PI_USER}@${pi_ip}:/tmp/raspberry_pi_nextcloud.sh" - - log_info "Running system configuration on Pi..." - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh configure" - - log_info "Installing Nextcloud on Pi..." - auto_generate_nextcloud_password - save_config - - log_success "Nextcloud admin user: $NEXTCLOUD_ADMIN_USER" - log_success "Nextcloud admin password: $NEXTCLOUD_ADMIN_PASSWORD" - - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S NEXTCLOUD_ADMIN_PASSWORD='$NEXTCLOUD_ADMIN_PASSWORD' NEXTCLOUD_ADMIN_USER='$NEXTCLOUD_ADMIN_USER' bash /tmp/raspberry_pi_nextcloud.sh install-local" - - log_info "Fixing Nextcloud issues..." - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no "${PI_USER}@${pi_ip}" \ - "echo '$PI_PASSWORD' | sudo -S bash /tmp/raspberry_pi_nextcloud.sh fix" - - log_success "========================================" - log_success "Remote Nextcloud installation complete!" - log_success "========================================" - echo - log_info "=== Access Information ===" - log_info "Nextcloud URL: https://$pi_ip" - log_info "Admin user: $NEXTCLOUD_ADMIN_USER" - log_info "Admin password: $NEXTCLOUD_ADMIN_PASSWORD" - log_info "All credentials saved in: $CONFIG_FILE" - echo - log_info "=== Trust the certificate ===" - log_info "Run: $0 install-ca" -} - -# ============================================================================= -# Install CA Certificate on Client -# ============================================================================= - -phase_install_ca() { - log_info "=== Installing Nextcloud CA Certificate ===" - - if [[ -z $PI_PASSWORD ]]; then - die "PI_PASSWORD not set. Run this after running install-remote or flash." - fi - - local pi_ip - pi_ip=$(discover_raspberry_pi) - - if [[ -z $pi_ip ]]; then - die "Failed to discover Raspberry Pi" - fi - - log_info "Downloading CA certificate from Pi..." - - local ca_file="/tmp/nextcloud-ca.crt" - - # Use SSH with sudo to cat the file (since it's in a protected directory) - sshpass -p "$PI_PASSWORD" ssh -o StrictHostKeyChecking=no \ - "${PI_USER}@${pi_ip}" "echo '$PI_PASSWORD' | sudo -S cat /etc/ssl/nextcloud/ca.crt" >"$ca_file" 2>/dev/null - - if [[ ! -f $ca_file ]] || [[ ! -s $ca_file ]]; then - die "Failed to download CA certificate" - fi - - log_success "CA certificate downloaded to: $ca_file" - - # Detect OS and install appropriately - if [[ -f /etc/arch-release ]]; then - log_info "Detected Arch Linux - installing CA..." - sudo cp "$ca_file" /etc/ca-certificates/trust-source/anchors/nextcloud-ca.crt - sudo trust extract-compat - log_success "CA installed in system trust store" - - elif [[ -f /etc/debian_version ]]; then - log_info "Detected Debian/Ubuntu - installing CA..." - sudo cp "$ca_file" /usr/local/share/ca-certificates/nextcloud-ca.crt - sudo update-ca-certificates - log_success "CA installed in system trust store" - - elif [[ -f /etc/redhat-release ]]; then - log_info "Detected RHEL/Fedora - installing CA..." - sudo cp "$ca_file" /etc/pki/ca-trust/source/anchors/nextcloud-ca.crt - sudo update-ca-trust - log_success "CA installed in system trust store" - - elif [[ "$(uname)" == "Darwin" ]]; then - log_info "Detected macOS - installing CA..." - sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$ca_file" - log_success "CA installed in system keychain" - - else - log_warning "Unknown OS - please install CA manually from: $ca_file" - fi - - # Install in browser certificate stores - log_info "Installing CA in browser certificate stores..." - - # Chrome/Chromium (uses NSS) - if [[ -d ~/.pki/nssdb ]] || command -v certutil &>/dev/null; then - mkdir -p ~/.pki/nssdb - if ! certutil -d sql:~/.pki/nssdb -L 2>/dev/null | grep -q "Nextcloud"; then - # Initialize NSS db if needed - certutil -d sql:~/.pki/nssdb -N --empty-password 2>/dev/null || true - if certutil -d sql:~/.pki/nssdb -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null; then - log_success "CA installed in Chrome/Chromium" - else - log_warning "Could not install in Chrome/Chromium NSS db" - fi - else - log_info "CA already installed in Chrome/Chromium" - fi - fi - - # Firefox (has its own profile NSS databases) - if [[ -d ~/.mozilla/firefox ]]; then - local installed=0 - for profile_dir in ~/.mozilla/firefox/*.default* ~/.mozilla/firefox/*.esr*; do - if [[ -d $profile_dir ]]; then - if ! certutil -d sql:"$profile_dir" -L 2>/dev/null | grep -q "Nextcloud"; then - certutil -d sql:"$profile_dir" -A -n "Nextcloud Home CA" -t "CT,C,C" -i "$ca_file" 2>/dev/null && - installed=1 - else - installed=1 - fi - fi - done - if [[ $installed -eq 1 ]]; then - log_success "CA installed in Firefox" - else - log_warning "Could not install in Firefox - you may need to import manually" - fi - fi - - # Add hostname to /etc/hosts if not present - if ! grep -q "$PI_HOSTNAME" /etc/hosts 2>/dev/null; then - log_info "Adding $PI_HOSTNAME to /etc/hosts..." - echo "$pi_ip $PI_HOSTNAME ${PI_HOSTNAME}.local" | sudo tee -a /etc/hosts >/dev/null - log_success "Added $PI_HOSTNAME to /etc/hosts" - else - log_info "$PI_HOSTNAME already in /etc/hosts" - fi - - # Verify - log_info "Verifying HTTPS connection..." - if curl -s --max-time 5 "https://$PI_HOSTNAME/status.php" 2>/dev/null | grep -q "installed"; then - log_success "HTTPS connection verified - no certificate warnings!" - else - log_warning "Could not verify HTTPS - you may need to restart your browser" - fi - - log_success "========================================" - log_success "CA Certificate installed!" - log_success "========================================" - echo - log_info "Access Nextcloud at: https://$PI_HOSTNAME" - log_info "Your browser should now trust the certificate without warnings." - echo - log_info "For other devices (phones, tablets, other computers):" - log_info " Download: https://$PI_HOSTNAME/ca/nextcloud-ca.crt" - log_info " Then install the certificate in your device's trust store." -} - -# ============================================================================= -# Main -# ============================================================================= - -show_help() { - cat <<'EOF' -Nextcloud Installation Script for Raspberry Pi - -Usage: ./raspberry_pi_nextcloud.sh - -Commands: - install-remote Install Nextcloud via SSH from your laptop (recommended) - setup-ssl-remote Setup Let's Encrypt SSL with DuckDNS (auto-trusted on all devices) - install-ca Install self-signed CA on this machine (alternative to setup-ssl) - configure Configure Pi system (run on Pi) - install-local Install Nextcloud (run on Pi) - fix Fix common Nextcloud issues (run on Pi) - setup-ssl Setup Let's Encrypt SSL (run on Pi) - help Show this help message - -The script will: -1. Configure the Raspberry Pi system (SSH hardening, firewall, etc.) -2. Install Apache, PHP, MariaDB, Redis -3. Download and install Nextcloud -4. Configure caching, background jobs, and security - -For HTTPS trusted on ALL devices automatically: - ./raspberry_pi_nextcloud.sh install-remote - ./raspberry_pi_nextcloud.sh setup-ssl-remote - - This uses DuckDNS (free) + Let's Encrypt for real trusted certificates. - Go to https://www.duckdns.org/ to get your free domain first. - -For self-signed certificates (requires manual CA install on each device): - ./raspberry_pi_nextcloud.sh install-remote - ./raspberry_pi_nextcloud.sh install-ca - -For local installation (on Pi): - sudo ./raspberry_pi_nextcloud.sh configure - sudo ./raspberry_pi_nextcloud.sh install-local - sudo ./raspberry_pi_nextcloud.sh fix - sudo ./raspberry_pi_nextcloud.sh setup-ssl -EOF -} main() { local command="${1:-help}" diff --git a/linux_configuration/scripts/single_use/utils/lib/bl9000_backup.sh b/linux_configuration/scripts/single_use/utils/lib/bl9000_backup.sh new file mode 100755 index 00000000..c08c299b --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/bl9000_backup.sh @@ -0,0 +1,165 @@ +#!/bin/bash +# Device data backup before rooting. +# +# Sourced by root_bl9000.sh; split out to keep bl9000_deps.sh under the +# 250-line cap. Sourced rather than run, so it inherits the caller's +# strict mode and the variables defined above the source line. + +backup_device_data() { + print_header "Backing Up Device Data" + + local backup_dir + backup_dir="${WORK_DIR}/backup_$(date +%Y%m%d_%H%M%S)" + mkdir -p "$backup_dir" + + log "Backup directory: $backup_dir" # Check device connection first + if ! adb get-state > /dev/null 2>&1; then + error "Device not connected. Please connect your device first." + return 1 + fi + + log "Starting comprehensive backup process..." + + # 1. Backup internal storage (DCIM, Pictures, Documents, Downloads, etc.) + log "Backing up internal storage (this may take a while)..." + local storage_dirs=("DCIM" "Pictures" "Documents" "Download" "Music" "Movies" "WhatsApp" "Telegram") + + for dir in "${storage_dirs[@]}"; do + if adb shell "[ -d /sdcard/$dir ]" 2> /dev/null; then + log " → Backing up /sdcard/$dir..." + if adb pull "/sdcard/$dir" "$backup_dir/$dir" 2>&1 | grep -v "^$"; then + log " ✓ $dir backed up successfully" + else + warn " ⚠ Could not backup $dir (may be empty or inaccessible)" + fi + fi + done + + # 2. Backup SMS/MMS (if possible) + log "Backing up SMS/MMS database..." + if adb shell "su -c 'cp /data/data/com.android.providers.telephony/databases/mmssms.db /sdcard/mmssms.db'" 2> /dev/null; then + adb pull /sdcard/mmssms.db "$backup_dir/mmssms.db" 2> /dev/null && log " ✓ SMS/MMS backed up" + adb shell "rm /sdcard/mmssms.db" 2> /dev/null || true + else + warn " ⚠ SMS/MMS backup requires root (skipping)" + fi + + # 3. Backup contacts + log "Backing up contacts..." + if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/contacts2.db /sdcard/contacts2.db'" 2> /dev/null; then + adb pull /sdcard/contacts2.db "$backup_dir/contacts2.db" 2> /dev/null && log " ✓ Contacts backed up" + adb shell "rm /sdcard/contacts2.db" 2> /dev/null || true + else + warn " ⚠ Contacts backup requires root (skipping)" + fi + + # 4. Backup call logs + log "Backing up call logs..." + if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/calllog.db /sdcard/calllog.db'" 2> /dev/null; then + adb pull /sdcard/calllog.db "$backup_dir/calllog.db" 2> /dev/null && log " ✓ Call logs backed up" + adb shell "rm /sdcard/calllog.db" 2> /dev/null || true + else + warn " ⚠ Call logs backup requires root (skipping)" + fi + + # 5. Backup app list + log "Backing up installed apps list..." + adb shell "pm list packages -f" > "$backup_dir/installed_apps.txt" + log " ✓ App list saved to installed_apps.txt" + + # 6. Backup APKs for user-installed apps (optional, can be large) + if confirm "Backup APK files for installed apps? (This can take a long time and use lots of space)"; then + log "Backing up user-installed APKs..." + local apk_dir="$backup_dir/apks" + mkdir -p "$apk_dir" + + # Get user-installed packages + local user_apps + user_apps=$(adb shell "pm list packages -3 -f" | sed 's/package://' | cut -d'=' -f2) + + local count=0 + while IFS= read -r pkg; do + if [[ -n $pkg ]]; then + log " → Backing up $pkg..." + local apk_path + apk_path=$(adb shell "pm path $pkg" | head -n1 | sed 's/package://') + if [[ -n $apk_path ]]; then + adb pull "$apk_path" "$apk_dir/${pkg}.apk" > /dev/null 2>&1 && count=$((count + 1)) + fi + fi + done <<< "$user_apps" + + log " ✓ Backed up $count APK files" + fi + + # 7. Full ADB backup (app data, if device supports it) + log "Creating full ADB backup (app data)..." + if confirm "Create full ADB backup? (You'll need to confirm on your device)"; then + echo + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " On your phone: Tap 'Back up my data' when prompted" + echo " You can set a password or leave it blank" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo + + if adb backup -apk -shared -all -system -f "$backup_dir/full_backup.ab"; then + log " ✓ Full ADB backup completed" + log " Note: Restore with: adb restore full_backup.ab" + else + warn " ⚠ ADB backup failed or was cancelled" + fi + fi + + # 8. Backup device info + log "Saving device information..." + { + echo "Device Backup Information" + echo "=========================" + echo "Date: $(date)" + echo + echo "Device Model: $(adb shell getprop ro.product.model | tr -d '\r\n')" + echo "Android Version: $(adb shell getprop ro.build.version.release | tr -d '\r\n')" + echo "Build Number: $(adb shell getprop ro.build.display.id | tr -d '\r\n')" + echo "Security Patch: $(adb shell getprop ro.build.version.security_patch | tr -d '\r\n')" + echo "Serial: $(adb shell getprop ro.serialno | tr -d '\r\n')" + echo + echo "Installed Apps:" + adb shell "pm list packages -3" | sed 's/package:/ - /' + } > "$backup_dir/device_info.txt" + + log " ✓ Device info saved" + + # Summary + local backup_size + backup_size=$(du -sh "$backup_dir" 2> /dev/null | cut -f1 || echo "unknown") + + echo + echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Backup Completed Successfully! ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}" + echo + log "Backup location: $backup_dir" + log "Backup size: $backup_size" + echo + echo "What was backed up:" + echo " ✓ Photos (DCIM)" + echo " ✓ Pictures" + echo " ✓ Documents" + echo " ✓ Downloads" + echo " ✓ Music" + echo " ✓ Movies" + echo " ✓ WhatsApp data (if present)" + echo " ✓ Telegram data (if present)" + echo " ✓ Installed apps list" + echo " ✓ Device information" + if [[ -f "$backup_dir/full_backup.ab" ]]; then + echo " ✓ Full app data backup" + fi + if [[ -d "$backup_dir/apks" ]]; then + echo " ✓ APK files" + fi + echo + log "Keep this backup safe! You'll need it to restore your data after rooting." + + return 0 +} diff --git a/linux_configuration/scripts/single_use/utils/lib/bl9000_deps.sh b/linux_configuration/scripts/single_use/utils/lib/bl9000_deps.sh new file mode 100755 index 00000000..6acee60b --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/bl9000_deps.sh @@ -0,0 +1,157 @@ +#!/bin/bash +# Dependency installation and prerequisite checks. +# +# Sourced by root_bl9000.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +install_dependencies() { + print_header "Installing Dependencies" + + local packages=() + local missing=() + + # Check for required commands + if ! command -v adb > /dev/null 2>&1; then + packages+=("android-tools") + missing+=("adb") + fi + + if ! command -v fastboot > /dev/null 2>&1 && ! pacman -Q android-tools > /dev/null 2>&1; then + packages+=("android-tools") + missing+=("fastboot") + fi + + if ! command -v unzip > /dev/null 2>&1; then + packages+=("unzip") + missing+=("unzip") + fi + + if ! command -v curl > /dev/null 2>&1; then + packages+=("curl") + missing+=("curl") + fi + + if ! command -v python3 > /dev/null 2>&1; then + packages+=("python") + missing+=("python3") + fi + + if ! command -v git > /dev/null 2>&1; then + packages+=("git") + missing+=("git") + fi + + # Check for libusb and fuse2 (needed for mtkclient) + if ! pacman -Q libusb > /dev/null 2>&1; then + packages+=("libusb") + missing+=("libusb") + fi + + if ! pacman -Q fuse2 > /dev/null 2>&1; then + packages+=("fuse2") + missing+=("fuse2") + fi + + # Check for python-protobuf (needed for boot image tools) + if ! python3 -c "import google.protobuf" 2> /dev/null; then + packages+=("python-protobuf") + missing+=("python-protobuf") + fi + + if [[ ${#missing[@]} -eq 0 ]]; then + log "All dependencies are already installed." + return 0 + fi + + log "Missing dependencies: ${missing[*]}" + + # Remove duplicates + readarray -t packages < <(printf '%s\n' "${packages[@]}" | sort -u) + + if ! confirm "Install missing packages: ${packages[*]}?"; then + die "Cannot proceed without required dependencies." + fi + + log "Installing packages: ${packages[*]}" + sudo pacman -S --needed --noconfirm "${packages[@]}" || die "Failed to install dependencies" + + log "Dependencies installed successfully." +} + +setup_udev_rules() { + print_header "Setting Up USB Access" + + local udev_file="/etc/udev/rules.d/51-android.rules" + local mtk_udev_dir="${WORK_DIR}/mtkclient/mtkclient/Setup/Linux" + + # Install MTKClient udev rules if mtkclient is present + if [[ -d "${WORK_DIR}/mtkclient" ]]; then + log "Installing MTKClient udev rules..." + if [[ -d $mtk_udev_dir ]]; then + sudo cp "$mtk_udev_dir"/*.rules /etc/udev/rules.d/ 2> /dev/null || warn "Failed to copy MTKClient rules" + fi + fi + + if [[ -f $udev_file ]]; then + log "Android udev rules already exist at $udev_file" + else + if ! confirm "Create udev rules for Android device access?"; then + warn "Skipping udev rules. You may need to run commands with sudo." + return 0 + fi + + log "Creating Android udev rules..." + + # Create comprehensive udev rules for Android devices + sudo tee "$udev_file" > /dev/null << 'EOF' +# Android Debug Bridge (ADB) devices +# Add your device's vendor ID if not listed + +# Google +SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="adbusers" +# MediaTek (common in BL9000) +SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", MODE="0666", GROUP="adbusers" +# Generic catch-all for Android devices +SUBSYSTEM=="usb", ATTR{idVendor}=="*", ATTR{idProduct}=="*", MODE="0666", GROUP="adbusers", SYMLINK+="android%n" +EOF + fi + + # Create adbusers group if it doesn't exist + if ! getent group adbusers > /dev/null; then + sudo groupadd -r adbusers + log "Created adbusers group" + fi + + # Add current user to adbusers and plugdev groups + if ! groups "$USER" | grep -q '\badbusers\b'; then + sudo usermod -aG adbusers "$USER" + log "Added $USER to adbusers group" + fi + + if ! getent group plugdev > /dev/null; then + sudo groupadd -r plugdev + fi + + if ! groups "$USER" | grep -q '\bplugdev\b'; then + sudo usermod -aG plugdev "$USER" + log "Added $USER to plugdev group" + fi + + if ! getent group dialout > /dev/null; then + sudo groupadd -r dialout + fi + + if ! groups "$USER" | grep -q '\bdialout\b'; then + sudo usermod -aG dialout "$USER" + log "Added $USER to dialout group" + warn "You need to log out and back in for group membership to take effect." + warn "Alternatively, run: newgrp dialout" + fi + + # Reload udev rules + sudo udevadm control --reload-rules + sudo udevadm trigger + + log "USB access configured successfully." +} diff --git a/linux_configuration/scripts/single_use/utils/lib/bl9000_device.sh b/linux_configuration/scripts/single_use/utils/lib/bl9000_device.sh new file mode 100755 index 00000000..fbb5b38b --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/bl9000_device.sh @@ -0,0 +1,194 @@ +#!/bin/bash +# Device detection and connection checks. +# +# Sourced by root_bl9000.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +check_device() { + print_header "Checking Device Connection" + + log "Starting ADB server..." + adb start-server > /dev/null 2>&1 || true + + log "Waiting for device..." + if ! adb wait-for-device; then + error "Failed to detect device via ADB." + echo + echo "Troubleshooting steps:" + echo "1. Make sure USB debugging is enabled on your phone" + echo " Settings → About Phone → Tap Build Number 7 times" + echo " Settings → Developer Options → Enable USB Debugging" + echo "2. Connect your phone via USB cable" + echo "3. Accept the 'Allow USB debugging' prompt on your phone" + echo "4. Run: adb devices" + echo + return 1 + fi + + local device_info + device_info=$(adb devices -l | grep -v "List of devices" | grep -v "^$" | head -n1) + + if [[ -z $device_info ]]; then + error "No device detected" + return 1 + fi + + log "Device connected: $device_info" + + # Check device properties + local model + model=$(adb shell getprop ro.product.model 2> /dev/null | tr -d '\r\n' || echo "Unknown") + log "Model: $model" + + local android_version + android_version=$(adb shell getprop ro.build.version.release 2> /dev/null | tr -d '\r\n' || echo "Unknown") + log "Android version: $android_version" + + local battery_level + battery_level=$(adb shell dumpsys battery | grep level | awk '{print $2}' | tr -d '\r\n' || echo "Unknown") + log "Battery level: ${battery_level}%" + + if [[ $battery_level != "Unknown" && $battery_level -lt 50 ]]; then + warn "Battery level is below 50%. Charge your phone before proceeding." + if ! confirm "Continue anyway?"; then + return 1 + fi + fi + + # Check if bootloader is unlocked + local unlock_status + unlock_status=$(adb shell getprop ro.boot.verifiedbootstate 2> /dev/null | tr -d '\r\n' || echo "unknown") + if [[ $unlock_status == "orange" || $unlock_status == "red" ]]; then + log "Bootloader unlock status: ${GREEN}UNLOCKED${NC}" + else + warn "Bootloader appears to be LOCKED. You'll need to unlock it to root." + fi + + # Check if OEM unlocking is enabled + local oem_unlock + oem_unlock=$(adb shell getprop sys.oem_unlock_allowed 2> /dev/null | tr -d '\r\n' || echo "unknown") + if [[ $oem_unlock == "1" ]]; then + log "OEM unlocking: ${GREEN}ENABLED${NC}" + else + warn "OEM unlocking is not enabled in Developer Options." + echo "Enable it at: Settings → Developer Options → OEM unlocking" + fi + + return 0 +} + +unlock_bootloader() { + print_header "Unlocking Bootloader" + + echo + echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${RED}║ WARNING ║${NC}" + echo -e "${RED}║ ║${NC}" + echo -e "${RED}║ Unlocking the bootloader will ERASE ALL DATA on your phone! ║${NC}" + echo -e "${RED}║ ║${NC}" + echo -e "${RED}║ This includes: ║${NC}" + echo -e "${RED}║ - All apps and app data ║${NC}" + echo -e "${RED}║ - Photos, videos, and files ║${NC}" + echo -e "${RED}║ - System settings ║${NC}" + echo -e "${RED}║ - Everything else on internal storage ║${NC}" + echo -e "${RED}║ ║${NC}" + echo -e "${RED}║ Make sure you have backed up important data! ║${NC}" + echo -e "${RED}╚═══════════════════════════════════════════════════════════════╝${NC}" + echo + + if ! confirm "Have you backed up all important data and want to proceed?"; then + log "Bootloader unlock cancelled by user." + return 1 + fi + + if ! confirm "Are you ABSOLUTELY SURE? This cannot be undone!"; then + log "Bootloader unlock cancelled by user." + return 1 + fi + + log "Rebooting device to bootloader..." + adb reboot bootloader || die "Failed to reboot to bootloader" + + log "Waiting for fastboot mode..." + sleep 5 + + if ! fastboot devices | grep -q .; then + error "Device not detected in fastboot mode." + echo + echo "If the device doesn't enter fastboot automatically:" + echo "1. Power off the phone completely" + echo "2. Hold Volume Down + Power buttons simultaneously" + echo "3. Release when you see the bootloader/fastboot screen" + echo "4. Run: fastboot devices" + echo + return 1 + fi + + log "Device in fastboot mode" + + # Check current bootloader status + local bl_status + bl_status=$(fastboot getvar unlocked 2>&1 | grep "unlocked:" | awk '{print $2}' || echo "unknown") + if [[ $bl_status == "yes" ]]; then + log "Bootloader is already unlocked." + fastboot reboot + return 0 + fi + + log "Attempting to unlock bootloader..." + + # Try different unlock commands (varies by device) + if fastboot flashing unlock 2>&1 | grep -qi "okay\|finished"; then + log "Bootloader unlock command sent successfully." + elif fastboot oem unlock 2>&1 | grep -qi "okay\|finished"; then + log "Bootloader unlock command sent successfully." + else + error "Bootloader unlock command may have failed." + echo + echo "On your phone:" + echo "1. Use volume buttons to select 'Unlock the bootloader'" + echo "2. Press power button to confirm" + echo + + if ! confirm "Did you complete the unlock on the device?"; then + fastboot reboot + return 1 + fi + fi + + log "Rebooting device..." + fastboot reboot || true + + log "Bootloader unlocked successfully!" + log "Device will now boot up and perform factory reset..." + log "Waiting for device to come back online..." + + sleep 10 + adb wait-for-device || true + + log "Please complete the initial setup on your phone, then re-enable USB debugging." + echo + + return 0 +} + +download_magisk() { + print_header "Downloading Magisk" + + local magisk_apk="$WORK_DIR/magisk.apk" + + if [[ -f $magisk_apk ]]; then + log "Magisk APK already downloaded at $magisk_apk" + return 0 + fi + + log "Downloading latest Magisk APK..." + if ! curl -L -o "$magisk_apk" "$MAGISK_APK_URL"; then + error "Failed to download Magisk APK" + return 1 + fi + + log "Magisk downloaded successfully: $magisk_apk" + return 0 +} diff --git a/linux_configuration/scripts/single_use/utils/lib/bl9000_magisk.sh b/linux_configuration/scripts/single_use/utils/lib/bl9000_magisk.sh new file mode 100755 index 00000000..43e9d54d --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/bl9000_magisk.sh @@ -0,0 +1,144 @@ +#!/bin/bash +# Boot image patching with Magisk. +# +# Sourced by root_bl9000.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +patch_boot_with_magisk() { + print_header "Patching Boot Image with Magisk" + + if [[ ! -f ${BOOT_IMG:-} ]]; then + die "Boot image not found: ${BOOT_IMG:-none}" + fi + + local magisk_apk="$WORK_DIR/magisk.apk" + if [[ ! -f $magisk_apk ]]; then + die "Magisk APK not found. Run download step first." + fi + + log "Checking if device is connected..." + if ! adb devices | grep -q "device$"; then + die "No device detected. Make sure USB debugging is enabled and device is connected." + fi + + log "Installing Magisk APK on device..." + if ! adb install -r "$magisk_apk" 2> /dev/null; then + warn "Magisk APK installation failed (may already be installed)" + fi + + log "Pushing boot image to device..." + adb push "$BOOT_IMG" /sdcard/Download/boot.img || die "Failed to push boot image" + + echo + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " MANUAL STEP REQUIRED" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo + echo "On your phone:" + echo "1. Open the Magisk app" + echo "2. Tap 'Install' next to Magisk" + echo "3. Select 'Select and Patch a File'" + echo "4. Navigate to Downloads and select boot.img" + echo "5. Tap 'Let's Go' and wait for patching to complete" + echo "6. The patched file will be saved as magisk_patched_*.img" + echo + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo + + if ! confirm "Have you completed patching the boot image in Magisk app?"; then + error "Patching cancelled by user" + return 1 + fi + + log "Pulling patched boot image from device..." + + local patched_img + patched_img=$(adb shell "ls /sdcard/Download/magisk_patched_*.img 2>/dev/null" | tr -d '\r\n' | head -n1 || echo "") + + if [[ -z $patched_img ]]; then + error "Could not find patched boot image on device." + echo "Please ensure the patching completed successfully in Magisk app." + return 1 + fi + + PATCHED_BOOT_IMG="$WORK_DIR/magisk_patched.img" + if ! adb pull "$patched_img" "$PATCHED_BOOT_IMG"; then + error "Failed to pull patched boot image" + return 1 + fi + + log "Patched boot image saved to: $PATCHED_BOOT_IMG" + return 0 +} + +flash_patched_boot() { + print_header "Flashing Patched Boot Image" + + if [[ ! -f ${PATCHED_BOOT_IMG:-} ]]; then + die "Patched boot image not found: ${PATCHED_BOOT_IMG:-none}" + fi + + echo + echo -e "${YELLOW}This will flash the patched boot image to your device.${NC}" + echo "Device uses A/B partitions - will flash to boot_a" + echo + + if ! confirm "Proceed with flashing?"; then + log "Flashing cancelled by user" + return 1 + fi + + log "Rebooting to bootloader..." + adb reboot bootloader || die "Failed to reboot to bootloader" + + log "Waiting for fastboot mode..." + sleep 5 + + if ! sudo fastboot devices | grep -q .; then + die "Device not detected in fastboot mode" + fi + + log "Flashing patched boot image to boot_a..." + if ! sudo fastboot flash boot_a "$PATCHED_BOOT_IMG"; then + error "Failed to flash boot image" + return 1 + fi + + log "Flashed successfully!" + log "Rebooting device..." + sudo fastboot reboot + + log "Waiting for device to boot..." + sleep 10 + adb wait-for-device || true + + echo + echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Root Process Complete! ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}" + echo + echo "Your BL9000 phone should now be rooted with Magisk!" + echo + echo "Next steps:" + echo "1. Open the Magisk app on your phone" + echo "2. Verify that it shows 'Installed' for both Magisk and App" + echo "3. Grant root access to apps as needed" + echo "4. Install Magisk modules if desired" + echo + echo "Note: Some banking and secure apps may not work with root." + echo " Use Magisk's DenyList feature to hide root from specific apps." + echo + + return 0 +} + +clean_work_dir() { + if [[ -d $WORK_DIR ]]; then + log "Removing working directory: $WORK_DIR" + rm -rf "$WORK_DIR" + log "Cleaned successfully" + else + log "Work directory doesn't exist: $WORK_DIR" + fi +} diff --git a/linux_configuration/scripts/single_use/utils/lib/bl9000_mtkclient.sh b/linux_configuration/scripts/single_use/utils/lib/bl9000_mtkclient.sh new file mode 100755 index 00000000..177b9497 --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/bl9000_mtkclient.sh @@ -0,0 +1,187 @@ +#!/bin/bash +# mtkclient checkout and setup. +# +# Sourced by root_bl9000.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +install_mtkclient() { + print_header "Installing MTKClient" + + local mtk_dir="${WORK_DIR}/mtkclient" + + if [[ -d $mtk_dir && -f "$mtk_dir/mtk.py" ]]; then + log "MTKClient already installed at $mtk_dir" + return 0 + fi + + if ! confirm "Install MTKClient for MediaTek boot image extraction?"; then + return 1 + fi + + log "Cloning MTKClient repository..." + if ! git clone https://github.com/bkerler/mtkclient "$mtk_dir"; then + error "Failed to clone MTKClient" + return 1 + fi + + log "Installing MTKClient Python dependencies..." + # Subshell so the cd cannot leak, and so a failure cannot strand the caller + # in $mtk_dir the way the old `cd -` did on an early return. + ( + cd "$mtk_dir" || exit 1 + python3 -m pip install --user -r requirements.txt || warn "Some dependencies may have failed to install" + python3 -m pip install --user . || warn "MTKClient installation may be incomplete" + ) || warn "MTKClient installation may be incomplete" + + log "MTKClient installed successfully" + return 0 +} + +extract_boot_with_mtkclient() { + print_header "Extracting Boot with MTKClient" + + local mtk_dir="${WORK_DIR}/mtkclient" + local boot_img="$WORK_DIR/boot.img" + local boot_a_img="$WORK_DIR/boot_a.img" + local vbmeta_a_img="$WORK_DIR/vbmeta_a.img" + + if [[ ! -d $mtk_dir ]]; then + error "MTKClient not installed. Run: $SCRIPT_NAME install-mtk" + return 1 + fi + + echo + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " MTKClient Boot ROM Mode Instructions" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo + echo "BEFORE continuing, you MUST:" + echo + echo " 1. Power off your phone COMPLETELY" + echo " 2. DISCONNECT the USB cable from your phone" + echo " 3. Have the USB cable ready in your hand" + echo + echo "When you press Enter:" + echo + echo " 4. Press and hold BOTH Volume buttons (Up + Down)" + echo " 5. While holding BOTH buttons, connect USB cable" + echo " 6. Keep holding until device is detected (may take 5-10 seconds)" + echo + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo + + if ! confirm "Phone is OFF and USB DISCONNECTED - ready to proceed?"; then + return 1 + fi + + log "Starting MTKClient NOW - quickly enter BROM mode!" + log "Hold BOTH volume buttons and connect USB cable..." + echo + + # Checked BEFORE the cd, so the early return needs no `cd -`. A subshell is + # not an option here: this function exports BOOT_IMG to its caller. + if [[ ! -d "$mtk_dir/venv" ]]; then + error "MTKClient virtual environment not found. Run: $SCRIPT_NAME install-mtk" + return 1 + fi + + local prev_dir="$PWD" + cd "$mtk_dir" || { + error "Cannot enter $mtk_dir" + return 1 + } + + # shellcheck source=/dev/null + source venv/bin/activate + # BL9000 uses A/B partitions, so extract boot_a and vbmeta_a + if python3 mtk.py r boot_a,vbmeta_a "$boot_a_img,$vbmeta_a_img"; then + log "Boot_a and vbmeta_a extracted successfully!" + + # Copy boot_a.img to boot.img for Magisk compatibility + cp "$boot_a_img" "$boot_img" + BOOT_IMG="$boot_img" + + log "Boot image ready for patching: $BOOT_IMG" + + # Reset device + log "Resetting device..." + python3 mtk.py reset || warn "Failed to reset device, please reboot manually" + + # Deactivate venv if function exists + type deactivate &> /dev/null && deactivate + + cd "$prev_dir" || true + return 0 + else + # Deactivate venv if function exists + type deactivate &> /dev/null && deactivate + + error "Failed to extract boot image with MTKClient" + cd "$prev_dir" || true + return 1 + fi +} + +extract_boot_image() { + print_header "Extracting Boot Image" + + local boot_img="$WORK_DIR/boot.img" + + if [[ -n ${BOOT_IMG:-} && -f $BOOT_IMG ]]; then + log "Using provided boot image: $BOOT_IMG" + cp "$BOOT_IMG" "$boot_img" + BOOT_IMG="$boot_img" + return 0 + fi + + log "Attempting to extract boot image from device..." + + # Method 1: Try MTKClient first (best for MediaTek devices) + if [[ -d "${WORK_DIR}/mtkclient" ]]; then + log "Trying MTKClient extraction..." + if extract_boot_with_mtkclient; then + return 0 + fi + warn "MTKClient extraction failed, trying ADB methods..." + fi + + # Method 2: Try to pull boot partition directly via ADB + local boot_partition + boot_partition=$(adb shell "find /dev/block -name boot | head -n1" 2> /dev/null | tr -d '\r\n' || echo "") + + if [[ -n $boot_partition ]]; then + log "Found boot partition: $boot_partition" + if adb pull "$boot_partition" "$boot_img" 2> /dev/null; then + log "Boot image extracted successfully" + BOOT_IMG="$boot_img" + return 0 + fi + fi + + # Method 3: Try to get boot partition via by-name + boot_partition=$(adb shell "ls /dev/block/by-name/boot*" 2> /dev/null | head -n1 | tr -d '\r\n' || echo "") + + if [[ -n $boot_partition ]]; then + log "Found boot partition: $boot_partition" + if adb shell "su -c 'dd if=$boot_partition of=/sdcard/boot.img'" 2> /dev/null && + adb pull /sdcard/boot.img "$boot_img" 2> /dev/null; then + adb shell rm /sdcard/boot.img 2> /dev/null || true + log "Boot image extracted successfully" + BOOT_IMG="$boot_img" + return 0 + fi + fi + + error "Failed to extract boot image automatically." + echo + echo "Manual extraction options:" + echo "1. Use MTKClient: $SCRIPT_NAME extract-mtk" + echo "2. Extract boot.img from your device's firmware package" + echo "3. Get boot.img from device manufacturer's official ROM" + echo + echo "Then run: $SCRIPT_NAME root --boot-img /path/to/boot.img" + echo + + return 1 +} diff --git a/linux_configuration/scripts/single_use/utils/lib/bl9000_usage.sh b/linux_configuration/scripts/single_use/utils/lib/bl9000_usage.sh new file mode 100755 index 00000000..25843ada --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/bl9000_usage.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# Usage text and the argument parsing help. +# +# Sourced by root_bl9000.sh; split out to keep it under the 250-line cap. +# Sourced rather than run, so it inherits the caller's strict mode and +# the helper functions and variables defined above the source line. + +usage() { + cat << EOF +Usage: $SCRIPT_NAME [OPTIONS] [COMMAND] + +Root BL9000 phone from Arch Linux using Magisk. + +Commands: + install-deps Install required dependencies (adb, fastboot, tools) + install-mtk Install MTKClient for MediaTek boot extraction + check Check device connection and prerequisites + backup Backup phone data before unlocking bootloader + unlock Unlock bootloader (WARNING: wipes all data!) + extract-mtk Extract boot.img using MTKClient (for MediaTek devices) + patch Patch boot.img with Magisk (manual step on phone) + flash [IMG] Flash patched boot image (uses magisk_patched.img or specified file) + auto-root Automated: extract -> patch -> flash in one command + root Extract boot, patch with Magisk, and flash + full Run complete rooting process (deps + unlock + root) + clean Remove temporary working directory + help Show this message + +Options: + -h, --help Show this message + --work-dir DIR Set working directory (default: $WORK_DIR) + --boot-img FILE Use existing boot.img instead of extracting from device + +Examples: + $SCRIPT_NAME install-deps # Install required tools + $SCRIPT_NAME install-mtk # Install MTKClient for boot extraction + $SCRIPT_NAME check # Verify device connection + $SCRIPT_NAME backup # Backup phone data first! + $SCRIPT_NAME extract-mtk # Extract boot.img with MTKClient + $SCRIPT_NAME auto-root # Automated rooting (extract + patch + flash) + $SCRIPT_NAME full # Complete rooting process + $SCRIPT_NAME root # Root only (assumes bootloader unlocked) + +WARNING: Unlocking the bootloader will ERASE ALL DATA on your phone! + Make sure to back up important data before proceeding. + +EOF +} + + + +run_full_process() { + print_header "BL9000 Full Root Process" + + log "Starting complete rooting process..." + + install_dependencies || die "Failed to install dependencies" + setup_udev_rules || true + + echo + if ! confirm "Continue to device check?"; then + die "Process cancelled by user" + fi + + check_device || die "Device check failed" + + echo + if ! confirm "Continue to backup device data?"; then + die "Process cancelled by user" + fi + + backup_device_data || warn "Backup failed or incomplete" + + echo + if ! confirm "Continue to bootloader unlock?"; then + die "Process cancelled by user" + fi + + unlock_bootloader || die "Bootloader unlock failed" + + echo + log "Please complete device setup and re-enable USB debugging, then press Enter..." + read -r + + check_device || die "Device check failed after unlock" + + download_magisk || die "Failed to download Magisk" + extract_boot_image || die "Failed to extract boot image" + patch_boot_with_magisk || die "Failed to patch boot image" + flash_patched_boot || die "Failed to flash patched boot" + + log "Full root process completed successfully!" +} + +run_root_only() { + print_header "BL9000 Root Process (Skip Unlock)" + + log "Starting root process (assuming bootloader is already unlocked)..." + + check_device || die "Device check failed" + download_magisk || die "Failed to download Magisk" + extract_boot_image || die "Failed to extract boot image" + patch_boot_with_magisk || die "Failed to patch boot image" + flash_patched_boot || die "Failed to flash patched boot" + + log "Root process completed successfully!" +} diff --git a/linux_configuration/scripts/single_use/utils/lib/docs_all.sh b/linux_configuration/scripts/single_use/utils/lib/docs_all.sh new file mode 100755 index 00000000..86e9779b --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/docs_all.sh @@ -0,0 +1,161 @@ +#!/bin/bash +# Cross-language lookup and content extraction. +# +# Sourced by lookup_docs.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +#============================================================================== +# Generic lookup (searches all languages) +#============================================================================== +lookup_all() { + local term="$1" + + # Try each language + for lang in python cpp js rust go shell; do + local result + result=$(lookup_$lang "$term" 2>/dev/null) + if [ -n "$result" ]; then + echo "$lang: $result" + fi + done +} + +#============================================================================== +# Parse Python import and lookup the actual imported item +#============================================================================== +parse_python_import() { + local import_line="$1" + + # Handle "from X import Y" format + if [[ $import_line =~ ^from[[:space:]]+([^[:space:]]+)[[:space:]]+import[[:space:]]+(.+) ]]; then + local module="${BASH_REMATCH[1]}" + local items="${BASH_REMATCH[2]}" + + # Clean up items (remove parentheses, commas, etc.) + items=$(echo "$items" | sed 's/[(),]//g' | awk '{print $1}') + + # Output: module and first imported item + echo "$module|$items" + return 0 + fi + + # Handle "import X" format + if [[ $import_line =~ ^import[[:space:]]+([^[:space:],]+) ]]; then + local module="${BASH_REMATCH[1]}" + echo "$module|" + return 0 + fi + + return 1 +} + +#============================================================================== +# Smart lookup for imports +#============================================================================== +lookup_import() { + local import_line="$1" + local lang="$2" + + case "$lang" in + python) + local parsed + parsed=$(parse_python_import "$import_line") + if [ -n "$parsed" ]; then + local module item + module=$(echo "$parsed" | cut -d'|' -f1) + item=$(echo "$parsed" | cut -d'|' -f2) + + # For "from X import Y", look up Y within module X's documentation + if [ -n "$item" ] && [ -n "$module" ]; then + local result + # Pass both item and module to lookup_python + result=$(lookup_python "$item" "$module") + if [ -n "$result" ]; then + echo "$result" + return 0 + fi + fi + + # Fall back to module documentation + lookup_python "$module" + fi + ;; + + c_cpp) + # Extract header name from #include
or #include "header" + local header + header=$(echo "$import_line" | sed -E 's/#include\s*[<"]([^">]+)[">]/\1/' | sed 's/\.h$//') + lookup_cpp "$header" + ;; + + javascript | typescript) + # Extract module from import/require + local module="" + # Match: from "module" or from 'module' + module=$(echo "$import_line" | grep -oP "from\s+['\"]\\K[^'\"]+") + if [ -z "$module" ]; then + # Match: require("module") or require('module') + module=$(echo "$import_line" | grep -oP "require\\(['\"]\\K[^'\"]+") + fi + [ -n "$module" ] && lookup_js "$module" + ;; + + *) + echo "Unknown language: $lang" + ;; + esac +} + +#============================================================================== +# Extract documentation content +#============================================================================== +extract_doc_content() { + local file="$1" + local term="$2" + local max_lines="${3:-20}" + + if [[ $file == *.html ]]; then + # Extract text from HTML, find section about term + if command -v html2text &>/dev/null; then + html2text "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" + elif command -v lynx &>/dev/null; then + lynx -dump -nolist "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" + else + # Basic extraction + sed 's/<[^>]*>//g' "$file" | grep -A"$max_lines" -i "$term" | head -"$max_lines" + fi + elif [[ $file == *.json ]]; then + # Pretty print JSON section + grep -A5 "\"$term\"" "$file" 2>/dev/null + else + # Plain text + grep -A"$max_lines" -i "$term" "$file" | head -"$max_lines" + fi +} + +#============================================================================== +# Main +#============================================================================== +usage() { + cat < [language] [options] + +Search offline documentation for a term. + +Languages: python, cpp, c_cpp, js, javascript, rust, go, shell, all + +Options: + --open Open the documentation file (requires xdg-open) + --extract Extract and display relevant content + --import Parse and lookup an import statement + --batch Process multiple terms from a file + +Examples: + $0 Path python # Find Path in Python docs + $0 vector cpp # Find vector in C++ docs + $0 map # Find map in all languages + $0 --import "from pathlib import Path" python + $0 --batch imports.txt python +EOF +} diff --git a/linux_configuration/scripts/single_use/utils/lib/docs_cpp.sh b/linux_configuration/scripts/single_use/utils/lib/docs_cpp.sh new file mode 100755 index 00000000..77010700 --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/docs_cpp.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# C++ documentation lookup. +# +# Sourced by lookup_docs.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +#============================================================================== +# C/C++ specific lookup +#============================================================================== +lookup_cpp() { + local term="$1" + local doc_dir="$DOCS_DIR/c_cpp" + local result="" + local desc="" + + # Resolve symlink if present (system package installs to c_cpp/system/) + [ -L "$doc_dir/system" ] && doc_dir="$doc_dir/system" + + # Common C headers + case "$term" in + stdio.h | stdio) + [ -f "$doc_dir/reference/cstdio/index.html" ] && result="$doc_dir/reference/cstdio/index.html" + [ -f "$doc_dir/en/c/io.html" ] && result="$doc_dir/en/c/io.html" + desc="C standard I/O header" + ;; + stdlib.h | stdlib) + [ -f "$doc_dir/reference/cstdlib/index.html" ] && result="$doc_dir/reference/cstdlib/index.html" + [ -f "$doc_dir/en/c/memory.html" ] && result="$doc_dir/en/c/memory.html" + desc="C standard library header" + ;; + string.h | cstring) + [ -f "$doc_dir/reference/cstring/index.html" ] && result="$doc_dir/reference/cstring/index.html" + desc="C string handling header" + ;; + math.h | cmath) + [ -f "$doc_dir/reference/cmath/index.html" ] && result="$doc_dir/reference/cmath/index.html" + desc="C math header" + ;; + esac + + # C++ STL containers + case "$term" in + vector) + [ -f "$doc_dir/reference/vector/index.html" ] && result="$doc_dir/reference/vector/index.html" + [ -f "$doc_dir/en/cpp/container/vector.html" ] && result="$doc_dir/en/cpp/container/vector.html" + desc="C++ std::vector container" + ;; + map) + [ -f "$doc_dir/reference/map/index.html" ] && result="$doc_dir/reference/map/index.html" + desc="C++ std::map container" + ;; + string) + [ -f "$doc_dir/reference/string/index.html" ] && result="$doc_dir/reference/string/index.html" + desc="C++ std::string" + ;; + iostream) + [ -f "$doc_dir/reference/iostream/index.html" ] && result="$doc_dir/reference/iostream/index.html" + desc="C++ iostream header" + ;; + esac + + # C keywords + case "$term" in + if | else | for | while | do | switch | case | break | continue | return | goto) + [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" + [ -f "$doc_dir/en/cpp/language/$term.html" ] && result="$doc_dir/en/cpp/language/$term.html" + desc="C/C++ keyword: $term" + ;; + int | char | float | double | void | long | short | unsigned | signed) + [ -f "$doc_dir/en/c/language/type.html" ] && result="$doc_dir/en/c/language/type.html" + desc="C/C++ type: $term" + ;; + struct | union | enum | typedef) + [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" + desc="C/C++ keyword: $term" + ;; + esac + + # Search in files if not found (use -L to follow symlinks) + if [ -z "$result" ]; then + local found + found=$(find -L "$doc_dir" -name "*${term}*" -type f 2>/dev/null | head -1) + if [ -n "$found" ]; then + result="$found" + desc="C/C++: $term" + fi + fi + + if [ -n "$result" ]; then + echo "$result|$desc" + fi +} diff --git a/linux_configuration/scripts/single_use/utils/lib/docs_js.sh b/linux_configuration/scripts/single_use/utils/lib/docs_js.sh new file mode 100755 index 00000000..821b64f7 --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/docs_js.sh @@ -0,0 +1,166 @@ +#!/bin/bash +# JavaScript documentation lookup. +# +# Sourced by lookup_docs.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +#============================================================================== +# JavaScript/MDN specific lookup +# Searches the cloned MDN content repository +#============================================================================== +lookup_js() { + local term="$1" + local mdn_dir="$DOCS_DIR/mdn-content/files/en-us" + + # Normalize term for searching + local term_lower + term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') + + # Handle common statement aliases (MDN uses if...else, try...catch, etc.) + local statement_aliases=( + "if:if...else" + "else:if...else" + "try:try...catch" + "catch:try...catch" + "finally:try...catch" + "do:do...while" + "while:while" + "for:for" + "switch:switch" + "case:switch" + "default:switch" + ) + + for alias in "${statement_aliases[@]}"; do + local key="${alias%%:*}" + local value="${alias##*:}" + if [ "$term_lower" = "$key" ]; then + local stmt_dir="$mdn_dir/web/javascript/reference/statements/$value" + if [ -d "$stmt_dir" ] && [ -f "$stmt_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$stmt_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$stmt_dir/index.md|${title:-$term}" + return 0 + fi + fi + done + + # Handle boolean/null literals + case "$term_lower" in + true | false) + local bool_dir="$mdn_dir/web/javascript/reference/global_objects/boolean" + if [ -d "$bool_dir" ] && [ -f "$bool_dir/index.md" ]; then + echo "$bool_dir/index.md|Boolean ($term)" + return 0 + fi + ;; + null) + local null_dir="$mdn_dir/web/javascript/reference/operators/null" + if [ -d "$null_dir" ] && [ -f "$null_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$null_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$null_dir/index.md|${title:-null}" + return 0 + fi + ;; + undefined) + local undef_dir="$mdn_dir/web/javascript/reference/global_objects/undefined" + if [ -d "$undef_dir" ] && [ -f "$undef_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$undef_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$undef_dir/index.md|${title:-undefined}" + return 0 + fi + ;; + esac + + # Search JavaScript reference directory structure (priority order) + local search_dirs=( + "$mdn_dir/web/javascript/reference/statements" + "$mdn_dir/web/javascript/reference/operators" + "$mdn_dir/web/javascript/reference/global_objects" + "$mdn_dir/web/javascript/reference/functions" + "$mdn_dir/web/javascript/reference/classes" + ) + + for search_dir in "${search_dirs[@]}"; do + if [ -d "$search_dir" ]; then + # Look for exact directory match (MDN uses directories with index.md) + local found_dir + found_dir=$(find "$search_dir" -maxdepth 2 -type d -iname "$term" 2>/dev/null | head -1) + if [ -n "$found_dir" ] && [ -f "$found_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found_dir/index.md|${title:-$term}" + return 0 + fi + fi + done + + # Search Web APIs - prioritize *_api directories for common terms + if [ -d "$mdn_dir/web/api" ]; then + # First try _api directory (e.g., fetch_api, console_api) + local api_dir="$mdn_dir/web/api/${term_lower}_api" + if [ -d "$api_dir" ] && [ -f "$api_dir/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$api_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$api_dir/index.md|${title:-$term API}" + return 0 + fi + + # Then try exact top-level API interface (e.g., Console, Document, Element) + local found + found=$(find "$mdn_dir/web/api" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + + # Try window/ for global functions like alert, confirm, etc. + local window_method="$mdn_dir/web/api/window/${term_lower}" + if [ -d "$window_method" ] && [ -f "$window_method/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$window_method/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$window_method/index.md|${title:-Window.$term()}" + return 0 + fi + + # Search nested API methods + found=$(find "$mdn_dir/web/api" -maxdepth 3 -type d -iname "$term" 2>/dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + fi + + # Now try partial matches in Global Objects (e.g., Array.from, Object.keys) + if [ -d "$mdn_dir/web/javascript/reference/global_objects" ]; then + local found + found=$(find "$mdn_dir/web/javascript/reference/global_objects" -maxdepth 2 -type d -iname "*${term}*" 2>/dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + fi + + # Glossary as last resort + if [ -d "$mdn_dir/glossary" ]; then + local found + found=$(find "$mdn_dir/glossary" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1) + if [ -n "$found" ] && [ -f "$found/index.md" ]; then + local title + title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') + echo "$found/index.md|${title:-$term}" + return 0 + fi + fi + + return 1 +} diff --git a/linux_configuration/scripts/single_use/utils/lib/docs_python.sh b/linux_configuration/scripts/single_use/utils/lib/docs_python.sh new file mode 100755 index 00000000..5ec79629 --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/docs_python.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# Python documentation lookup. +# +# Sourced by lookup_docs.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +#============================================================================== +# Python-specific lookup +#============================================================================== +lookup_python() { + local term="$1" + local in_module="$2" # Optional: look for term within this module + local doc_dir="$DOCS_DIR/python" + local result="" + local desc="" + + # Normalize term (preserve case for True/False/None) + local term_lower + term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') + + # If looking for a term within a specific module + if [ -n "$in_module" ]; then + local module_lower + module_lower=$(echo "$in_module" | tr '[:upper:]' '[:lower:]') + + if [ -f "$doc_dir/library/${module_lower}.html" ]; then + # Find anchor for the specific item in the module + local anchor + anchor=$(grep -oP "id=\"[^\"]*${term}[^\"]*\"" "$doc_dir/library/${module_lower}.html" 2>/dev/null | head -1 | sed 's/id="//;s/"//') + + if [ -n "$anchor" ]; then + result="$doc_dir/library/${module_lower}.html#$anchor" + desc="Python: $in_module.$term" + else + # Just link to the module + result="$doc_dir/library/${module_lower}.html" + desc="Python: $term in module $in_module" + fi + echo "$result|$desc" + return 0 + fi + fi + + local _kw + _kw="$(python_keyword_doc "$doc_dir" "$term" "$term_lower")" + IFS=$'\t' read -r result desc <<<"$_kw" + + # Verify file exists for keyword lookups + if [ -n "$result" ] && [ ! -f "${result%%#*}" ]; then + result="" + desc="" + fi + + #-------------------------------------------------------------------------- + # PRIORITY 2: Check if it's a module (pathlib, os, sys, etc.) + #-------------------------------------------------------------------------- + if [ -z "$result" ] && [ -f "$doc_dir/library/${term_lower}.html" ]; then + result="$doc_dir/library/${term_lower}.html" + desc="Python module: $term" + fi + + #-------------------------------------------------------------------------- + # PRIORITY 3: Built-in functions (library/functions.html) + #-------------------------------------------------------------------------- + if [ -z "$result" ] && [ -f "$doc_dir/library/functions.html" ]; then + if grep -q "id=\"$term_lower\"" "$doc_dir/library/functions.html" 2>/dev/null; then + result="$doc_dir/library/functions.html#$term_lower" + desc="Python built-in function: $term" + fi + fi + + #-------------------------------------------------------------------------- + # PRIORITY 4: Built-in types (library/stdtypes.html) + #-------------------------------------------------------------------------- + if [ -z "$result" ]; then + case "$term_lower" in + str | string) + result="$doc_dir/library/stdtypes.html#str" + desc="Python: str type" + ;; + int | integer) + result="$doc_dir/library/stdtypes.html#int" + desc="Python: int type" + ;; + float) + result="$doc_dir/library/stdtypes.html#float" + desc="Python: float type" + ;; + list) + result="$doc_dir/library/stdtypes.html#list" + desc="Python: list type" + ;; + dict | dictionary) + result="$doc_dir/library/stdtypes.html#dict" + desc="Python: dict type" + ;; + set) + result="$doc_dir/library/stdtypes.html#set" + desc="Python: set type" + ;; + tuple) + result="$doc_dir/library/stdtypes.html#tuple" + desc="Python: tuple type" + ;; + bool | boolean) + result="$doc_dir/library/stdtypes.html#boolean-values" + desc="Python: bool type" + ;; + bytes) + result="$doc_dir/library/stdtypes.html#bytes" + desc="Python: bytes type" + ;; + esac + fi + + #-------------------------------------------------------------------------- + # PRIORITY 5: Check for class/function in module docs (exact id match) + #-------------------------------------------------------------------------- + if [ -z "$result" ]; then + local found_in + # Look for exact id match first + found_in=$(grep -l "id=\"$term\"" "$doc_dir/library/"*.html 2>/dev/null | head -1) + if [ -n "$found_in" ]; then + result="$found_in#$term" + local module + module=$(basename "$found_in" .html) + desc="Python: $term in module $module" + fi + fi + + #-------------------------------------------------------------------------- + # PRIORITY 6: Search in index + #-------------------------------------------------------------------------- + if [ -z "$result" ] && [ -f "$INDEX_DIR/python_index.txt" ]; then + local index_match + index_match=$(grep -i "^$term " "$INDEX_DIR/python_index.txt" 2>/dev/null | head -1) + if [ -n "$index_match" ]; then + result=$(echo "$index_match" | cut -d' ' -f2-) + desc="Python: $term (from index)" + fi + fi + + # NO full-text search fallback - it produces garbage results + # If we can't find a specific doc, return nothing (will fall back to online) + + if [ -n "$result" ]; then + echo "$result|$desc" + fi +} diff --git a/linux_configuration/scripts/single_use/utils/lib/docs_python_keywords.sh b/linux_configuration/scripts/single_use/utils/lib/docs_python_keywords.sh new file mode 100755 index 00000000..5730b4bb --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/docs_python_keywords.sh @@ -0,0 +1,167 @@ +#!/bin/bash +# Mapping Python keywords and operators to exact documentation anchors. +# +# Sourced by lookup_docs.sh; split out to keep docs_python.sh under the +# 250-line cap. Emits "\t" so the caller keeps its own +# control flow; it reads doc_dir/term/term_lower from the caller's scope. + +python_keyword_doc() { + local doc_dir="$1" term="$2" term_lower="$3" + local result="" desc="" + + #-------------------------------------------------------------------------- + # PRIORITY 1: Python keywords - map to exact documentation locations + #-------------------------------------------------------------------------- + + # Compound statements (reference/compound_stmts.html) + case "$term_lower" in + if | elif | else) + result="$doc_dir/reference/compound_stmts.html#if" + desc="Python: if statement" + ;; + for) + result="$doc_dir/reference/compound_stmts.html#for" + desc="Python: for statement" + ;; + while) + result="$doc_dir/reference/compound_stmts.html#while" + desc="Python: while statement" + ;; + def) + result="$doc_dir/reference/compound_stmts.html#def" + desc="Python: function definition" + ;; + class) + result="$doc_dir/reference/compound_stmts.html#class" + desc="Python: class definition" + ;; + try | except | finally) + result="$doc_dir/reference/compound_stmts.html#try" + desc="Python: try statement" + ;; + with) + result="$doc_dir/reference/compound_stmts.html#with" + desc="Python: with statement" + ;; + async) + result="$doc_dir/reference/compound_stmts.html#async" + desc="Python: async definition" + ;; + match | case) + result="$doc_dir/reference/compound_stmts.html#match" + desc="Python: match statement" + ;; + esac + + # Simple statements (reference/simple_stmts.html) + if [ -z "$result" ]; then + case "$term_lower" in + return) + result="$doc_dir/reference/simple_stmts.html#return" + desc="Python: return statement" + ;; + pass) + result="$doc_dir/reference/simple_stmts.html#pass" + desc="Python: pass statement" + ;; + break) + result="$doc_dir/reference/simple_stmts.html#break" + desc="Python: break statement" + ;; + continue) + result="$doc_dir/reference/simple_stmts.html#continue" + desc="Python: continue statement" + ;; + import | from) + result="$doc_dir/reference/simple_stmts.html#import" + desc="Python: import statement" + ;; + raise) + result="$doc_dir/reference/simple_stmts.html#raise" + desc="Python: raise statement" + ;; + assert) + result="$doc_dir/reference/simple_stmts.html#assert" + desc="Python: assert statement" + ;; + yield) + result="$doc_dir/reference/simple_stmts.html#yield" + desc="Python: yield expression" + ;; + del) + result="$doc_dir/reference/simple_stmts.html#del" + desc="Python: del statement" + ;; + global) + result="$doc_dir/reference/simple_stmts.html#global" + desc="Python: global statement" + ;; + nonlocal) + result="$doc_dir/reference/simple_stmts.html#nonlocal" + desc="Python: nonlocal statement" + ;; + type) + result="$doc_dir/reference/simple_stmts.html#type" + desc="Python: type alias statement" + ;; + esac + fi + + # Expressions/operators (reference/expressions.html) + if [ -z "$result" ]; then + case "$term_lower" in + and) + result="$doc_dir/reference/expressions.html#and" + desc="Python: and operator" + ;; + or) + result="$doc_dir/reference/expressions.html#or" + desc="Python: or operator" + ;; + not) + result="$doc_dir/reference/expressions.html#not" + desc="Python: not operator" + ;; + in) + result="$doc_dir/reference/expressions.html#in" + desc="Python: in operator" + ;; + is) + result="$doc_dir/reference/expressions.html#is" + desc="Python: is operator" + ;; + lambda) + result="$doc_dir/reference/expressions.html#lambda" + desc="Python: lambda expression" + ;; + await) + result="$doc_dir/reference/expressions.html#await" + desc="Python: await expression" + ;; + esac + fi + + # Built-in constants (library/constants.html) - case-sensitive! + if [ -z "$result" ]; then + case "$term" in + True | False) + result="$doc_dir/library/constants.html#$term" + desc="Python: $term constant" + ;; + None) + result="$doc_dir/library/constants.html#None" + desc="Python: None constant" + ;; + Ellipsis) + result="$doc_dir/library/constants.html#Ellipsis" + desc="Python: Ellipsis constant" + ;; + NotImplemented) + result="$doc_dir/library/constants.html#NotImplemented" + desc="Python: NotImplemented constant" + ;; + esac + fi + + printf '%s\t%s\n' "$result" "$desc" +} diff --git a/linux_configuration/scripts/single_use/utils/lib/docs_rust.sh b/linux_configuration/scripts/single_use/utils/lib/docs_rust.sh new file mode 100755 index 00000000..a1b1b308 --- /dev/null +++ b/linux_configuration/scripts/single_use/utils/lib/docs_rust.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# Rust documentation lookup. +# +# Sourced by lookup_docs.sh; split out to keep it under the 250-line +# cap. Sourced rather than run, so it inherits the caller's strict mode +# and the variables defined above the source line. + +#============================================================================== +# Rust specific lookup +#============================================================================== +lookup_rust() { + local term="$1" + local result="" + local desc="" + + if command -v rustup &>/dev/null; then + # Use rustup doc to get path + local rust_doc_path + rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null) + + # Search in std docs + if [ -d "$rust_doc_path/std" ]; then + local found + found=$(find "$rust_doc_path/std" -name "*${term}*" -type f 2>/dev/null | head -1) + if [ -n "$found" ]; then + result="$found" + desc="Rust: $term" + fi + fi + fi + + if [ -n "$result" ]; then + echo "$result|$desc" + fi +} + +#============================================================================== +# Go specific lookup +#============================================================================== +lookup_go() { + local term="$1" + local result="" + local desc="" + + if command -v go &>/dev/null; then + # Check if it's a stdlib package + if go doc "$term" &>/dev/null; then + result="go doc $term" + desc="Go package: $term (use 'go doc $term' to view)" + fi + fi + + if [ -n "$result" ]; then + echo "$result|$desc" + fi +} + +#============================================================================== +# Shell specific lookup +#============================================================================== +lookup_shell() { + local term="$1" + local doc_dir="$DOCS_DIR/shell" + local result="" + local desc="" + + # Check bash builtins + if [ -f "$doc_dir/bash_builtins.txt" ]; then + if grep -q "=== $term ===" "$doc_dir/bash_builtins.txt" 2>/dev/null; then + result="$doc_dir/bash_builtins.txt" + desc="Bash builtin: $term" + fi + fi + + # Check common commands + if [ -z "$result" ] && [ -f "$doc_dir/common_commands.txt" ]; then + if grep -q "^$term" "$doc_dir/common_commands.txt" 2>/dev/null; then + local cmd_desc + cmd_desc=$(grep "^$term" "$doc_dir/common_commands.txt" | head -1) + result="$doc_dir/common_commands.txt" + desc="Shell command: $cmd_desc" + fi + fi + + # Try man page + if [ -z "$result" ]; then + local man_path + man_path=$(man -w "$term" 2>/dev/null) + if [ -n "$man_path" ]; then + result="man $term" + desc="Manual page: $term (use 'man $term' to view)" + fi + fi + + if [ -n "$result" ]; then + echo "$result|$desc" + fi +} diff --git a/linux_configuration/scripts/single_use/utils/lookup_docs.sh b/linux_configuration/scripts/single_use/utils/lookup_docs.sh index 30b0133e..480a05f5 100755 --- a/linux_configuration/scripts/single_use/utils/lookup_docs.sh +++ b/linux_configuration/scripts/single_use/utils/lookup_docs.sh @@ -35,793 +35,22 @@ else NC='' fi -#============================================================================== -# Python-specific lookup -#============================================================================== -lookup_python() { - local term="$1" - local in_module="$2" # Optional: look for term within this module - local doc_dir="$DOCS_DIR/python" - local result="" - local desc="" +SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" + +# shellcheck source=lib/docs_python_keywords.sh +source "$SCRIPT_DIR/lib/docs_python_keywords.sh" +# shellcheck source=lib/docs_python.sh +source "$SCRIPT_DIR/lib/docs_python.sh" +# shellcheck source=lib/docs_cpp.sh +source "$SCRIPT_DIR/lib/docs_cpp.sh" +# shellcheck source=lib/docs_js.sh +source "$SCRIPT_DIR/lib/docs_js.sh" +# shellcheck source=lib/docs_rust.sh +source "$SCRIPT_DIR/lib/docs_rust.sh" +# shellcheck source=lib/docs_all.sh +source "$SCRIPT_DIR/lib/docs_all.sh" - # Normalize term (preserve case for True/False/None) - local term_lower - term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') - # If looking for a term within a specific module - if [ -n "$in_module" ]; then - local module_lower - module_lower=$(echo "$in_module" | tr '[:upper:]' '[:lower:]') - - if [ -f "$doc_dir/library/${module_lower}.html" ]; then - # Find anchor for the specific item in the module - local anchor - anchor=$(grep -oP "id=\"[^\"]*${term}[^\"]*\"" "$doc_dir/library/${module_lower}.html" 2>/dev/null | head -1 | sed 's/id="//;s/"//') - - if [ -n "$anchor" ]; then - result="$doc_dir/library/${module_lower}.html#$anchor" - desc="Python: $in_module.$term" - else - # Just link to the module - result="$doc_dir/library/${module_lower}.html" - desc="Python: $term in module $in_module" - fi - echo "$result|$desc" - return 0 - fi - fi - - #-------------------------------------------------------------------------- - # PRIORITY 1: Python keywords - map to exact documentation locations - #-------------------------------------------------------------------------- - - # Compound statements (reference/compound_stmts.html) - case "$term_lower" in - if | elif | else) - result="$doc_dir/reference/compound_stmts.html#if" - desc="Python: if statement" - ;; - for) - result="$doc_dir/reference/compound_stmts.html#for" - desc="Python: for statement" - ;; - while) - result="$doc_dir/reference/compound_stmts.html#while" - desc="Python: while statement" - ;; - def) - result="$doc_dir/reference/compound_stmts.html#def" - desc="Python: function definition" - ;; - class) - result="$doc_dir/reference/compound_stmts.html#class" - desc="Python: class definition" - ;; - try | except | finally) - result="$doc_dir/reference/compound_stmts.html#try" - desc="Python: try statement" - ;; - with) - result="$doc_dir/reference/compound_stmts.html#with" - desc="Python: with statement" - ;; - async) - result="$doc_dir/reference/compound_stmts.html#async" - desc="Python: async definition" - ;; - match | case) - result="$doc_dir/reference/compound_stmts.html#match" - desc="Python: match statement" - ;; - esac - - # Simple statements (reference/simple_stmts.html) - if [ -z "$result" ]; then - case "$term_lower" in - return) - result="$doc_dir/reference/simple_stmts.html#return" - desc="Python: return statement" - ;; - pass) - result="$doc_dir/reference/simple_stmts.html#pass" - desc="Python: pass statement" - ;; - break) - result="$doc_dir/reference/simple_stmts.html#break" - desc="Python: break statement" - ;; - continue) - result="$doc_dir/reference/simple_stmts.html#continue" - desc="Python: continue statement" - ;; - import | from) - result="$doc_dir/reference/simple_stmts.html#import" - desc="Python: import statement" - ;; - raise) - result="$doc_dir/reference/simple_stmts.html#raise" - desc="Python: raise statement" - ;; - assert) - result="$doc_dir/reference/simple_stmts.html#assert" - desc="Python: assert statement" - ;; - yield) - result="$doc_dir/reference/simple_stmts.html#yield" - desc="Python: yield expression" - ;; - del) - result="$doc_dir/reference/simple_stmts.html#del" - desc="Python: del statement" - ;; - global) - result="$doc_dir/reference/simple_stmts.html#global" - desc="Python: global statement" - ;; - nonlocal) - result="$doc_dir/reference/simple_stmts.html#nonlocal" - desc="Python: nonlocal statement" - ;; - type) - result="$doc_dir/reference/simple_stmts.html#type" - desc="Python: type alias statement" - ;; - esac - fi - - # Expressions/operators (reference/expressions.html) - if [ -z "$result" ]; then - case "$term_lower" in - and) - result="$doc_dir/reference/expressions.html#and" - desc="Python: and operator" - ;; - or) - result="$doc_dir/reference/expressions.html#or" - desc="Python: or operator" - ;; - not) - result="$doc_dir/reference/expressions.html#not" - desc="Python: not operator" - ;; - in) - result="$doc_dir/reference/expressions.html#in" - desc="Python: in operator" - ;; - is) - result="$doc_dir/reference/expressions.html#is" - desc="Python: is operator" - ;; - lambda) - result="$doc_dir/reference/expressions.html#lambda" - desc="Python: lambda expression" - ;; - await) - result="$doc_dir/reference/expressions.html#await" - desc="Python: await expression" - ;; - esac - fi - - # Built-in constants (library/constants.html) - case-sensitive! - if [ -z "$result" ]; then - case "$term" in - True | False) - result="$doc_dir/library/constants.html#$term" - desc="Python: $term constant" - ;; - None) - result="$doc_dir/library/constants.html#None" - desc="Python: None constant" - ;; - Ellipsis) - result="$doc_dir/library/constants.html#Ellipsis" - desc="Python: Ellipsis constant" - ;; - NotImplemented) - result="$doc_dir/library/constants.html#NotImplemented" - desc="Python: NotImplemented constant" - ;; - esac - fi - - # Verify file exists for keyword lookups - if [ -n "$result" ] && [ ! -f "${result%%#*}" ]; then - result="" - desc="" - fi - - #-------------------------------------------------------------------------- - # PRIORITY 2: Check if it's a module (pathlib, os, sys, etc.) - #-------------------------------------------------------------------------- - if [ -z "$result" ] && [ -f "$doc_dir/library/${term_lower}.html" ]; then - result="$doc_dir/library/${term_lower}.html" - desc="Python module: $term" - fi - - #-------------------------------------------------------------------------- - # PRIORITY 3: Built-in functions (library/functions.html) - #-------------------------------------------------------------------------- - if [ -z "$result" ] && [ -f "$doc_dir/library/functions.html" ]; then - if grep -q "id=\"$term_lower\"" "$doc_dir/library/functions.html" 2>/dev/null; then - result="$doc_dir/library/functions.html#$term_lower" - desc="Python built-in function: $term" - fi - fi - - #-------------------------------------------------------------------------- - # PRIORITY 4: Built-in types (library/stdtypes.html) - #-------------------------------------------------------------------------- - if [ -z "$result" ]; then - case "$term_lower" in - str | string) - result="$doc_dir/library/stdtypes.html#str" - desc="Python: str type" - ;; - int | integer) - result="$doc_dir/library/stdtypes.html#int" - desc="Python: int type" - ;; - float) - result="$doc_dir/library/stdtypes.html#float" - desc="Python: float type" - ;; - list) - result="$doc_dir/library/stdtypes.html#list" - desc="Python: list type" - ;; - dict | dictionary) - result="$doc_dir/library/stdtypes.html#dict" - desc="Python: dict type" - ;; - set) - result="$doc_dir/library/stdtypes.html#set" - desc="Python: set type" - ;; - tuple) - result="$doc_dir/library/stdtypes.html#tuple" - desc="Python: tuple type" - ;; - bool | boolean) - result="$doc_dir/library/stdtypes.html#boolean-values" - desc="Python: bool type" - ;; - bytes) - result="$doc_dir/library/stdtypes.html#bytes" - desc="Python: bytes type" - ;; - esac - fi - - #-------------------------------------------------------------------------- - # PRIORITY 5: Check for class/function in module docs (exact id match) - #-------------------------------------------------------------------------- - if [ -z "$result" ]; then - local found_in - # Look for exact id match first - found_in=$(grep -l "id=\"$term\"" "$doc_dir/library/"*.html 2>/dev/null | head -1) - if [ -n "$found_in" ]; then - result="$found_in#$term" - local module - module=$(basename "$found_in" .html) - desc="Python: $term in module $module" - fi - fi - - #-------------------------------------------------------------------------- - # PRIORITY 6: Search in index - #-------------------------------------------------------------------------- - if [ -z "$result" ] && [ -f "$INDEX_DIR/python_index.txt" ]; then - local index_match - index_match=$(grep -i "^$term " "$INDEX_DIR/python_index.txt" 2>/dev/null | head -1) - if [ -n "$index_match" ]; then - result=$(echo "$index_match" | cut -d' ' -f2-) - desc="Python: $term (from index)" - fi - fi - - # NO full-text search fallback - it produces garbage results - # If we can't find a specific doc, return nothing (will fall back to online) - - if [ -n "$result" ]; then - echo "$result|$desc" - fi -} - -#============================================================================== -# C/C++ specific lookup -#============================================================================== -lookup_cpp() { - local term="$1" - local doc_dir="$DOCS_DIR/c_cpp" - local result="" - local desc="" - - # Resolve symlink if present (system package installs to c_cpp/system/) - [ -L "$doc_dir/system" ] && doc_dir="$doc_dir/system" - - # Common C headers - case "$term" in - stdio.h | stdio) - [ -f "$doc_dir/reference/cstdio/index.html" ] && result="$doc_dir/reference/cstdio/index.html" - [ -f "$doc_dir/en/c/io.html" ] && result="$doc_dir/en/c/io.html" - desc="C standard I/O header" - ;; - stdlib.h | stdlib) - [ -f "$doc_dir/reference/cstdlib/index.html" ] && result="$doc_dir/reference/cstdlib/index.html" - [ -f "$doc_dir/en/c/memory.html" ] && result="$doc_dir/en/c/memory.html" - desc="C standard library header" - ;; - string.h | cstring) - [ -f "$doc_dir/reference/cstring/index.html" ] && result="$doc_dir/reference/cstring/index.html" - desc="C string handling header" - ;; - math.h | cmath) - [ -f "$doc_dir/reference/cmath/index.html" ] && result="$doc_dir/reference/cmath/index.html" - desc="C math header" - ;; - esac - - # C++ STL containers - case "$term" in - vector) - [ -f "$doc_dir/reference/vector/index.html" ] && result="$doc_dir/reference/vector/index.html" - [ -f "$doc_dir/en/cpp/container/vector.html" ] && result="$doc_dir/en/cpp/container/vector.html" - desc="C++ std::vector container" - ;; - map) - [ -f "$doc_dir/reference/map/index.html" ] && result="$doc_dir/reference/map/index.html" - desc="C++ std::map container" - ;; - string) - [ -f "$doc_dir/reference/string/index.html" ] && result="$doc_dir/reference/string/index.html" - desc="C++ std::string" - ;; - iostream) - [ -f "$doc_dir/reference/iostream/index.html" ] && result="$doc_dir/reference/iostream/index.html" - desc="C++ iostream header" - ;; - esac - - # C keywords - case "$term" in - if | else | for | while | do | switch | case | break | continue | return | goto) - [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" - [ -f "$doc_dir/en/cpp/language/$term.html" ] && result="$doc_dir/en/cpp/language/$term.html" - desc="C/C++ keyword: $term" - ;; - int | char | float | double | void | long | short | unsigned | signed) - [ -f "$doc_dir/en/c/language/type.html" ] && result="$doc_dir/en/c/language/type.html" - desc="C/C++ type: $term" - ;; - struct | union | enum | typedef) - [ -f "$doc_dir/en/c/language/$term.html" ] && result="$doc_dir/en/c/language/$term.html" - desc="C/C++ keyword: $term" - ;; - esac - - # Search in files if not found (use -L to follow symlinks) - if [ -z "$result" ]; then - local found - found=$(find -L "$doc_dir" -name "*${term}*" -type f 2>/dev/null | head -1) - if [ -n "$found" ]; then - result="$found" - desc="C/C++: $term" - fi - fi - - if [ -n "$result" ]; then - echo "$result|$desc" - fi -} - -#============================================================================== -# JavaScript/MDN specific lookup -# Searches the cloned MDN content repository -#============================================================================== -lookup_js() { - local term="$1" - local mdn_dir="$DOCS_DIR/mdn-content/files/en-us" - - # Normalize term for searching - local term_lower - term_lower=$(echo "$term" | tr '[:upper:]' '[:lower:]') - - # Handle common statement aliases (MDN uses if...else, try...catch, etc.) - local statement_aliases=( - "if:if...else" - "else:if...else" - "try:try...catch" - "catch:try...catch" - "finally:try...catch" - "do:do...while" - "while:while" - "for:for" - "switch:switch" - "case:switch" - "default:switch" - ) - - for alias in "${statement_aliases[@]}"; do - local key="${alias%%:*}" - local value="${alias##*:}" - if [ "$term_lower" = "$key" ]; then - local stmt_dir="$mdn_dir/web/javascript/reference/statements/$value" - if [ -d "$stmt_dir" ] && [ -f "$stmt_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$stmt_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$stmt_dir/index.md|${title:-$term}" - return 0 - fi - fi - done - - # Handle boolean/null literals - case "$term_lower" in - true | false) - local bool_dir="$mdn_dir/web/javascript/reference/global_objects/boolean" - if [ -d "$bool_dir" ] && [ -f "$bool_dir/index.md" ]; then - echo "$bool_dir/index.md|Boolean ($term)" - return 0 - fi - ;; - null) - local null_dir="$mdn_dir/web/javascript/reference/operators/null" - if [ -d "$null_dir" ] && [ -f "$null_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$null_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$null_dir/index.md|${title:-null}" - return 0 - fi - ;; - undefined) - local undef_dir="$mdn_dir/web/javascript/reference/global_objects/undefined" - if [ -d "$undef_dir" ] && [ -f "$undef_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$undef_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$undef_dir/index.md|${title:-undefined}" - return 0 - fi - ;; - esac - - # Search JavaScript reference directory structure (priority order) - local search_dirs=( - "$mdn_dir/web/javascript/reference/statements" - "$mdn_dir/web/javascript/reference/operators" - "$mdn_dir/web/javascript/reference/global_objects" - "$mdn_dir/web/javascript/reference/functions" - "$mdn_dir/web/javascript/reference/classes" - ) - - for search_dir in "${search_dirs[@]}"; do - if [ -d "$search_dir" ]; then - # Look for exact directory match (MDN uses directories with index.md) - local found_dir - found_dir=$(find "$search_dir" -maxdepth 2 -type d -iname "$term" 2>/dev/null | head -1) - if [ -n "$found_dir" ] && [ -f "$found_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found_dir/index.md|${title:-$term}" - return 0 - fi - fi - done - - # Search Web APIs - prioritize *_api directories for common terms - if [ -d "$mdn_dir/web/api" ]; then - # First try _api directory (e.g., fetch_api, console_api) - local api_dir="$mdn_dir/web/api/${term_lower}_api" - if [ -d "$api_dir" ] && [ -f "$api_dir/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$api_dir/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$api_dir/index.md|${title:-$term API}" - return 0 - fi - - # Then try exact top-level API interface (e.g., Console, Document, Element) - local found - found=$(find "$mdn_dir/web/api" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi - - # Try window/ for global functions like alert, confirm, etc. - local window_method="$mdn_dir/web/api/window/${term_lower}" - if [ -d "$window_method" ] && [ -f "$window_method/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$window_method/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$window_method/index.md|${title:-Window.$term()}" - return 0 - fi - - # Search nested API methods - found=$(find "$mdn_dir/web/api" -maxdepth 3 -type d -iname "$term" 2>/dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi - fi - - # Now try partial matches in Global Objects (e.g., Array.from, Object.keys) - if [ -d "$mdn_dir/web/javascript/reference/global_objects" ]; then - local found - found=$(find "$mdn_dir/web/javascript/reference/global_objects" -maxdepth 2 -type d -iname "*${term}*" 2>/dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi - fi - - # Glossary as last resort - if [ -d "$mdn_dir/glossary" ]; then - local found - found=$(find "$mdn_dir/glossary" -maxdepth 1 -type d -iname "$term" 2>/dev/null | head -1) - if [ -n "$found" ] && [ -f "$found/index.md" ]; then - local title - title=$(grep -m1 "^title:" "$found/index.md" 2>/dev/null | sed 's/^title:\s*//' | tr -d '"') - echo "$found/index.md|${title:-$term}" - return 0 - fi - fi - - return 1 -} - -#============================================================================== -# Rust specific lookup -#============================================================================== -lookup_rust() { - local term="$1" - local result="" - local desc="" - - if command -v rustup &>/dev/null; then - # Use rustup doc to get path - local rust_doc_path - rust_doc_path=$(rustup doc --path 2>/dev/null | head -1 | xargs dirname 2>/dev/null) - - # Search in std docs - if [ -d "$rust_doc_path/std" ]; then - local found - found=$(find "$rust_doc_path/std" -name "*${term}*" -type f 2>/dev/null | head -1) - if [ -n "$found" ]; then - result="$found" - desc="Rust: $term" - fi - fi - fi - - if [ -n "$result" ]; then - echo "$result|$desc" - fi -} - -#============================================================================== -# Go specific lookup -#============================================================================== -lookup_go() { - local term="$1" - local result="" - local desc="" - - if command -v go &>/dev/null; then - # Check if it's a stdlib package - if go doc "$term" &>/dev/null; then - result="go doc $term" - desc="Go package: $term (use 'go doc $term' to view)" - fi - fi - - if [ -n "$result" ]; then - echo "$result|$desc" - fi -} - -#============================================================================== -# Shell specific lookup -#============================================================================== -lookup_shell() { - local term="$1" - local doc_dir="$DOCS_DIR/shell" - local result="" - local desc="" - - # Check bash builtins - if [ -f "$doc_dir/bash_builtins.txt" ]; then - if grep -q "=== $term ===" "$doc_dir/bash_builtins.txt" 2>/dev/null; then - result="$doc_dir/bash_builtins.txt" - desc="Bash builtin: $term" - fi - fi - - # Check common commands - if [ -z "$result" ] && [ -f "$doc_dir/common_commands.txt" ]; then - if grep -q "^$term" "$doc_dir/common_commands.txt" 2>/dev/null; then - local cmd_desc - cmd_desc=$(grep "^$term" "$doc_dir/common_commands.txt" | head -1) - result="$doc_dir/common_commands.txt" - desc="Shell command: $cmd_desc" - fi - fi - - # Try man page - if [ -z "$result" ]; then - local man_path - man_path=$(man -w "$term" 2>/dev/null) - if [ -n "$man_path" ]; then - result="man $term" - desc="Manual page: $term (use 'man $term' to view)" - fi - fi - - if [ -n "$result" ]; then - echo "$result|$desc" - fi -} - -#============================================================================== -# Generic lookup (searches all languages) -#============================================================================== -lookup_all() { - local term="$1" - - # Try each language - for lang in python cpp js rust go shell; do - local result - result=$(lookup_$lang "$term" 2>/dev/null) - if [ -n "$result" ]; then - echo "$lang: $result" - fi - done -} - -#============================================================================== -# Parse Python import and lookup the actual imported item -#============================================================================== -parse_python_import() { - local import_line="$1" - - # Handle "from X import Y" format - if [[ $import_line =~ ^from[[:space:]]+([^[:space:]]+)[[:space:]]+import[[:space:]]+(.+) ]]; then - local module="${BASH_REMATCH[1]}" - local items="${BASH_REMATCH[2]}" - - # Clean up items (remove parentheses, commas, etc.) - items=$(echo "$items" | sed 's/[(),]//g' | awk '{print $1}') - - # Output: module and first imported item - echo "$module|$items" - return 0 - fi - - # Handle "import X" format - if [[ $import_line =~ ^import[[:space:]]+([^[:space:],]+) ]]; then - local module="${BASH_REMATCH[1]}" - echo "$module|" - return 0 - fi - - return 1 -} - -#============================================================================== -# Smart lookup for imports -#============================================================================== -lookup_import() { - local import_line="$1" - local lang="$2" - - case "$lang" in - python) - local parsed - parsed=$(parse_python_import "$import_line") - if [ -n "$parsed" ]; then - local module item - module=$(echo "$parsed" | cut -d'|' -f1) - item=$(echo "$parsed" | cut -d'|' -f2) - - # For "from X import Y", look up Y within module X's documentation - if [ -n "$item" ] && [ -n "$module" ]; then - local result - # Pass both item and module to lookup_python - result=$(lookup_python "$item" "$module") - if [ -n "$result" ]; then - echo "$result" - return 0 - fi - fi - - # Fall back to module documentation - lookup_python "$module" - fi - ;; - - c_cpp) - # Extract header name from #include
or #include "header" - local header - header=$(echo "$import_line" | sed -E 's/#include\s*[<"]([^">]+)[">]/\1/' | sed 's/\.h$//') - lookup_cpp "$header" - ;; - - javascript | typescript) - # Extract module from import/require - local module="" - # Match: from "module" or from 'module' - module=$(echo "$import_line" | grep -oP "from\s+['\"]\\K[^'\"]+") - if [ -z "$module" ]; then - # Match: require("module") or require('module') - module=$(echo "$import_line" | grep -oP "require\\(['\"]\\K[^'\"]+") - fi - [ -n "$module" ] && lookup_js "$module" - ;; - - *) - echo "Unknown language: $lang" - ;; - esac -} - -#============================================================================== -# Extract documentation content -#============================================================================== -extract_doc_content() { - local file="$1" - local term="$2" - local max_lines="${3:-20}" - - if [[ $file == *.html ]]; then - # Extract text from HTML, find section about term - if command -v html2text &>/dev/null; then - html2text "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" - elif command -v lynx &>/dev/null; then - lynx -dump -nolist "$file" 2>/dev/null | grep -A"$max_lines" -i "$term" | head -"$max_lines" - else - # Basic extraction - sed 's/<[^>]*>//g' "$file" | grep -A"$max_lines" -i "$term" | head -"$max_lines" - fi - elif [[ $file == *.json ]]; then - # Pretty print JSON section - grep -A5 "\"$term\"" "$file" 2>/dev/null - else - # Plain text - grep -A"$max_lines" -i "$term" "$file" | head -"$max_lines" - fi -} - -#============================================================================== -# Main -#============================================================================== -usage() { - cat < [language] [options] - -Search offline documentation for a term. - -Languages: python, cpp, c_cpp, js, javascript, rust, go, shell, all - -Options: - --open Open the documentation file (requires xdg-open) - --extract Extract and display relevant content - --import Parse and lookup an import statement - --batch Process multiple terms from a file - -Examples: - $0 Path python # Find Path in Python docs - $0 vector cpp # Find vector in C++ docs - $0 map # Find map in all languages - $0 --import "from pathlib import Path" python - $0 --batch imports.txt python -EOF -} main() { if [ $# -eq 0 ]; then diff --git a/linux_configuration/scripts/single_use/utils/root_bl9000.sh b/linux_configuration/scripts/single_use/utils/root_bl9000.sh index 56a511f9..53fdc9ae 100755 --- a/linux_configuration/scripts/single_use/utils/root_bl9000.sh +++ b/linux_configuration/scripts/single_use/utils/root_bl9000.sh @@ -89,914 +89,19 @@ require_non_root() { fi } -usage() { - cat << EOF -Usage: $SCRIPT_NAME [OPTIONS] [COMMAND] - -Root BL9000 phone from Arch Linux using Magisk. - -Commands: - install-deps Install required dependencies (adb, fastboot, tools) - install-mtk Install MTKClient for MediaTek boot extraction - check Check device connection and prerequisites - backup Backup phone data before unlocking bootloader - unlock Unlock bootloader (WARNING: wipes all data!) - extract-mtk Extract boot.img using MTKClient (for MediaTek devices) - patch Patch boot.img with Magisk (manual step on phone) - flash [IMG] Flash patched boot image (uses magisk_patched.img or specified file) - auto-root Automated: extract -> patch -> flash in one command - root Extract boot, patch with Magisk, and flash - full Run complete rooting process (deps + unlock + root) - clean Remove temporary working directory - help Show this message - -Options: - -h, --help Show this message - --work-dir DIR Set working directory (default: $WORK_DIR) - --boot-img FILE Use existing boot.img instead of extracting from device - -Examples: - $SCRIPT_NAME install-deps # Install required tools - $SCRIPT_NAME install-mtk # Install MTKClient for boot extraction - $SCRIPT_NAME check # Verify device connection - $SCRIPT_NAME backup # Backup phone data first! - $SCRIPT_NAME extract-mtk # Extract boot.img with MTKClient - $SCRIPT_NAME auto-root # Automated rooting (extract + patch + flash) - $SCRIPT_NAME full # Complete rooting process - $SCRIPT_NAME root # Root only (assumes bootloader unlocked) - -WARNING: Unlocking the bootloader will ERASE ALL DATA on your phone! - Make sure to back up important data before proceeding. - -EOF -} - -install_dependencies() { - print_header "Installing Dependencies" - - local packages=() - local missing=() - - # Check for required commands - if ! command -v adb > /dev/null 2>&1; then - packages+=("android-tools") - missing+=("adb") - fi - - if ! command -v fastboot > /dev/null 2>&1 && ! pacman -Q android-tools > /dev/null 2>&1; then - packages+=("android-tools") - missing+=("fastboot") - fi - - if ! command -v unzip > /dev/null 2>&1; then - packages+=("unzip") - missing+=("unzip") - fi - - if ! command -v curl > /dev/null 2>&1; then - packages+=("curl") - missing+=("curl") - fi - - if ! command -v python3 > /dev/null 2>&1; then - packages+=("python") - missing+=("python3") - fi - - if ! command -v git > /dev/null 2>&1; then - packages+=("git") - missing+=("git") - fi - - # Check for libusb and fuse2 (needed for mtkclient) - if ! pacman -Q libusb > /dev/null 2>&1; then - packages+=("libusb") - missing+=("libusb") - fi - - if ! pacman -Q fuse2 > /dev/null 2>&1; then - packages+=("fuse2") - missing+=("fuse2") - fi - - # Check for python-protobuf (needed for boot image tools) - if ! python3 -c "import google.protobuf" 2> /dev/null; then - packages+=("python-protobuf") - missing+=("python-protobuf") - fi - - if [[ ${#missing[@]} -eq 0 ]]; then - log "All dependencies are already installed." - return 0 - fi - - log "Missing dependencies: ${missing[*]}" - - # Remove duplicates - readarray -t packages < <(printf '%s\n' "${packages[@]}" | sort -u) - - if ! confirm "Install missing packages: ${packages[*]}?"; then - die "Cannot proceed without required dependencies." - fi - - log "Installing packages: ${packages[*]}" - sudo pacman -S --needed --noconfirm "${packages[@]}" || die "Failed to install dependencies" - - log "Dependencies installed successfully." -} - -setup_udev_rules() { - print_header "Setting Up USB Access" - - local udev_file="/etc/udev/rules.d/51-android.rules" - local mtk_udev_dir="${WORK_DIR}/mtkclient/mtkclient/Setup/Linux" - - # Install MTKClient udev rules if mtkclient is present - if [[ -d "${WORK_DIR}/mtkclient" ]]; then - log "Installing MTKClient udev rules..." - if [[ -d $mtk_udev_dir ]]; then - sudo cp "$mtk_udev_dir"/*.rules /etc/udev/rules.d/ 2> /dev/null || warn "Failed to copy MTKClient rules" - fi - fi - - if [[ -f $udev_file ]]; then - log "Android udev rules already exist at $udev_file" - else - if ! confirm "Create udev rules for Android device access?"; then - warn "Skipping udev rules. You may need to run commands with sudo." - return 0 - fi - - log "Creating Android udev rules..." - - # Create comprehensive udev rules for Android devices - sudo tee "$udev_file" > /dev/null << 'EOF' -# Android Debug Bridge (ADB) devices -# Add your device's vendor ID if not listed - -# Google -SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="adbusers" -# MediaTek (common in BL9000) -SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", MODE="0666", GROUP="adbusers" -# Generic catch-all for Android devices -SUBSYSTEM=="usb", ATTR{idVendor}=="*", ATTR{idProduct}=="*", MODE="0666", GROUP="adbusers", SYMLINK+="android%n" -EOF - fi - - # Create adbusers group if it doesn't exist - if ! getent group adbusers > /dev/null; then - sudo groupadd -r adbusers - log "Created adbusers group" - fi - - # Add current user to adbusers and plugdev groups - if ! groups "$USER" | grep -q '\badbusers\b'; then - sudo usermod -aG adbusers "$USER" - log "Added $USER to adbusers group" - fi - - if ! getent group plugdev > /dev/null; then - sudo groupadd -r plugdev - fi - - if ! groups "$USER" | grep -q '\bplugdev\b'; then - sudo usermod -aG plugdev "$USER" - log "Added $USER to plugdev group" - fi - - if ! getent group dialout > /dev/null; then - sudo groupadd -r dialout - fi - - if ! groups "$USER" | grep -q '\bdialout\b'; then - sudo usermod -aG dialout "$USER" - log "Added $USER to dialout group" - warn "You need to log out and back in for group membership to take effect." - warn "Alternatively, run: newgrp dialout" - fi - - # Reload udev rules - sudo udevadm control --reload-rules - sudo udevadm trigger - - log "USB access configured successfully." -} - -backup_device_data() { - print_header "Backing Up Device Data" - - local backup_dir - backup_dir="${WORK_DIR}/backup_$(date +%Y%m%d_%H%M%S)" - mkdir -p "$backup_dir" - - log "Backup directory: $backup_dir" # Check device connection first - if ! adb get-state > /dev/null 2>&1; then - error "Device not connected. Please connect your device first." - return 1 - fi - - log "Starting comprehensive backup process..." - - # 1. Backup internal storage (DCIM, Pictures, Documents, Downloads, etc.) - log "Backing up internal storage (this may take a while)..." - local storage_dirs=("DCIM" "Pictures" "Documents" "Download" "Music" "Movies" "WhatsApp" "Telegram") - - for dir in "${storage_dirs[@]}"; do - if adb shell "[ -d /sdcard/$dir ]" 2> /dev/null; then - log " → Backing up /sdcard/$dir..." - if adb pull "/sdcard/$dir" "$backup_dir/$dir" 2>&1 | grep -v "^$"; then - log " ✓ $dir backed up successfully" - else - warn " ⚠ Could not backup $dir (may be empty or inaccessible)" - fi - fi - done - - # 2. Backup SMS/MMS (if possible) - log "Backing up SMS/MMS database..." - if adb shell "su -c 'cp /data/data/com.android.providers.telephony/databases/mmssms.db /sdcard/mmssms.db'" 2> /dev/null; then - adb pull /sdcard/mmssms.db "$backup_dir/mmssms.db" 2> /dev/null && log " ✓ SMS/MMS backed up" - adb shell "rm /sdcard/mmssms.db" 2> /dev/null || true - else - warn " ⚠ SMS/MMS backup requires root (skipping)" - fi - - # 3. Backup contacts - log "Backing up contacts..." - if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/contacts2.db /sdcard/contacts2.db'" 2> /dev/null; then - adb pull /sdcard/contacts2.db "$backup_dir/contacts2.db" 2> /dev/null && log " ✓ Contacts backed up" - adb shell "rm /sdcard/contacts2.db" 2> /dev/null || true - else - warn " ⚠ Contacts backup requires root (skipping)" - fi - - # 4. Backup call logs - log "Backing up call logs..." - if adb shell "su -c 'cp /data/data/com.android.providers.contacts/databases/calllog.db /sdcard/calllog.db'" 2> /dev/null; then - adb pull /sdcard/calllog.db "$backup_dir/calllog.db" 2> /dev/null && log " ✓ Call logs backed up" - adb shell "rm /sdcard/calllog.db" 2> /dev/null || true - else - warn " ⚠ Call logs backup requires root (skipping)" - fi - - # 5. Backup app list - log "Backing up installed apps list..." - adb shell "pm list packages -f" > "$backup_dir/installed_apps.txt" - log " ✓ App list saved to installed_apps.txt" - - # 6. Backup APKs for user-installed apps (optional, can be large) - if confirm "Backup APK files for installed apps? (This can take a long time and use lots of space)"; then - log "Backing up user-installed APKs..." - local apk_dir="$backup_dir/apks" - mkdir -p "$apk_dir" - - # Get user-installed packages - local user_apps - user_apps=$(adb shell "pm list packages -3 -f" | sed 's/package://' | cut -d'=' -f2) - - local count=0 - while IFS= read -r pkg; do - if [[ -n $pkg ]]; then - log " → Backing up $pkg..." - local apk_path - apk_path=$(adb shell "pm path $pkg" | head -n1 | sed 's/package://') - if [[ -n $apk_path ]]; then - adb pull "$apk_path" "$apk_dir/${pkg}.apk" > /dev/null 2>&1 && count=$((count + 1)) - fi - fi - done <<< "$user_apps" - - log " ✓ Backed up $count APK files" - fi - - # 7. Full ADB backup (app data, if device supports it) - log "Creating full ADB backup (app data)..." - if confirm "Create full ADB backup? (You'll need to confirm on your device)"; then - echo - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " On your phone: Tap 'Back up my data' when prompted" - echo " You can set a password or leave it blank" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo - - if adb backup -apk -shared -all -system -f "$backup_dir/full_backup.ab"; then - log " ✓ Full ADB backup completed" - log " Note: Restore with: adb restore full_backup.ab" - else - warn " ⚠ ADB backup failed or was cancelled" - fi - fi - - # 8. Backup device info - log "Saving device information..." - { - echo "Device Backup Information" - echo "=========================" - echo "Date: $(date)" - echo - echo "Device Model: $(adb shell getprop ro.product.model | tr -d '\r\n')" - echo "Android Version: $(adb shell getprop ro.build.version.release | tr -d '\r\n')" - echo "Build Number: $(adb shell getprop ro.build.display.id | tr -d '\r\n')" - echo "Security Patch: $(adb shell getprop ro.build.version.security_patch | tr -d '\r\n')" - echo "Serial: $(adb shell getprop ro.serialno | tr -d '\r\n')" - echo - echo "Installed Apps:" - adb shell "pm list packages -3" | sed 's/package:/ - /' - } > "$backup_dir/device_info.txt" - - log " ✓ Device info saved" - - # Summary - local backup_size - backup_size=$(du -sh "$backup_dir" 2> /dev/null | cut -f1 || echo "unknown") - - echo - echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ Backup Completed Successfully! ║${NC}" - echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}" - echo - log "Backup location: $backup_dir" - log "Backup size: $backup_size" - echo - echo "What was backed up:" - echo " ✓ Photos (DCIM)" - echo " ✓ Pictures" - echo " ✓ Documents" - echo " ✓ Downloads" - echo " ✓ Music" - echo " ✓ Movies" - echo " ✓ WhatsApp data (if present)" - echo " ✓ Telegram data (if present)" - echo " ✓ Installed apps list" - echo " ✓ Device information" - if [[ -f "$backup_dir/full_backup.ab" ]]; then - echo " ✓ Full app data backup" - fi - if [[ -d "$backup_dir/apks" ]]; then - echo " ✓ APK files" - fi - echo - log "Keep this backup safe! You'll need it to restore your data after rooting." - - return 0 -} - -check_device() { - print_header "Checking Device Connection" - - log "Starting ADB server..." - adb start-server > /dev/null 2>&1 || true - - log "Waiting for device..." - if ! adb wait-for-device; then - error "Failed to detect device via ADB." - echo - echo "Troubleshooting steps:" - echo "1. Make sure USB debugging is enabled on your phone" - echo " Settings → About Phone → Tap Build Number 7 times" - echo " Settings → Developer Options → Enable USB Debugging" - echo "2. Connect your phone via USB cable" - echo "3. Accept the 'Allow USB debugging' prompt on your phone" - echo "4. Run: adb devices" - echo - return 1 - fi - - local device_info - device_info=$(adb devices -l | grep -v "List of devices" | grep -v "^$" | head -n1) - - if [[ -z $device_info ]]; then - error "No device detected" - return 1 - fi - - log "Device connected: $device_info" - - # Check device properties - local model - model=$(adb shell getprop ro.product.model 2> /dev/null | tr -d '\r\n' || echo "Unknown") - log "Model: $model" - - local android_version - android_version=$(adb shell getprop ro.build.version.release 2> /dev/null | tr -d '\r\n' || echo "Unknown") - log "Android version: $android_version" - - local battery_level - battery_level=$(adb shell dumpsys battery | grep level | awk '{print $2}' | tr -d '\r\n' || echo "Unknown") - log "Battery level: ${battery_level}%" - - if [[ $battery_level != "Unknown" && $battery_level -lt 50 ]]; then - warn "Battery level is below 50%. Charge your phone before proceeding." - if ! confirm "Continue anyway?"; then - return 1 - fi - fi - - # Check if bootloader is unlocked - local unlock_status - unlock_status=$(adb shell getprop ro.boot.verifiedbootstate 2> /dev/null | tr -d '\r\n' || echo "unknown") - if [[ $unlock_status == "orange" || $unlock_status == "red" ]]; then - log "Bootloader unlock status: ${GREEN}UNLOCKED${NC}" - else - warn "Bootloader appears to be LOCKED. You'll need to unlock it to root." - fi - - # Check if OEM unlocking is enabled - local oem_unlock - oem_unlock=$(adb shell getprop sys.oem_unlock_allowed 2> /dev/null | tr -d '\r\n' || echo "unknown") - if [[ $oem_unlock == "1" ]]; then - log "OEM unlocking: ${GREEN}ENABLED${NC}" - else - warn "OEM unlocking is not enabled in Developer Options." - echo "Enable it at: Settings → Developer Options → OEM unlocking" - fi - - return 0 -} - -unlock_bootloader() { - print_header "Unlocking Bootloader" - - echo - echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}" - echo -e "${RED}║ WARNING ║${NC}" - echo -e "${RED}║ ║${NC}" - echo -e "${RED}║ Unlocking the bootloader will ERASE ALL DATA on your phone! ║${NC}" - echo -e "${RED}║ ║${NC}" - echo -e "${RED}║ This includes: ║${NC}" - echo -e "${RED}║ - All apps and app data ║${NC}" - echo -e "${RED}║ - Photos, videos, and files ║${NC}" - echo -e "${RED}║ - System settings ║${NC}" - echo -e "${RED}║ - Everything else on internal storage ║${NC}" - echo -e "${RED}║ ║${NC}" - echo -e "${RED}║ Make sure you have backed up important data! ║${NC}" - echo -e "${RED}╚═══════════════════════════════════════════════════════════════╝${NC}" - echo - - if ! confirm "Have you backed up all important data and want to proceed?"; then - log "Bootloader unlock cancelled by user." - return 1 - fi - - if ! confirm "Are you ABSOLUTELY SURE? This cannot be undone!"; then - log "Bootloader unlock cancelled by user." - return 1 - fi - - log "Rebooting device to bootloader..." - adb reboot bootloader || die "Failed to reboot to bootloader" - - log "Waiting for fastboot mode..." - sleep 5 - - if ! fastboot devices | grep -q .; then - error "Device not detected in fastboot mode." - echo - echo "If the device doesn't enter fastboot automatically:" - echo "1. Power off the phone completely" - echo "2. Hold Volume Down + Power buttons simultaneously" - echo "3. Release when you see the bootloader/fastboot screen" - echo "4. Run: fastboot devices" - echo - return 1 - fi - - log "Device in fastboot mode" - - # Check current bootloader status - local bl_status - bl_status=$(fastboot getvar unlocked 2>&1 | grep "unlocked:" | awk '{print $2}' || echo "unknown") - if [[ $bl_status == "yes" ]]; then - log "Bootloader is already unlocked." - fastboot reboot - return 0 - fi - - log "Attempting to unlock bootloader..." - - # Try different unlock commands (varies by device) - if fastboot flashing unlock 2>&1 | grep -qi "okay\|finished"; then - log "Bootloader unlock command sent successfully." - elif fastboot oem unlock 2>&1 | grep -qi "okay\|finished"; then - log "Bootloader unlock command sent successfully." - else - error "Bootloader unlock command may have failed." - echo - echo "On your phone:" - echo "1. Use volume buttons to select 'Unlock the bootloader'" - echo "2. Press power button to confirm" - echo - - if ! confirm "Did you complete the unlock on the device?"; then - fastboot reboot - return 1 - fi - fi - - log "Rebooting device..." - fastboot reboot || true - - log "Bootloader unlocked successfully!" - log "Device will now boot up and perform factory reset..." - log "Waiting for device to come back online..." - - sleep 10 - adb wait-for-device || true - - log "Please complete the initial setup on your phone, then re-enable USB debugging." - echo - - return 0 -} - -download_magisk() { - print_header "Downloading Magisk" - - local magisk_apk="$WORK_DIR/magisk.apk" - - if [[ -f $magisk_apk ]]; then - log "Magisk APK already downloaded at $magisk_apk" - return 0 - fi - - log "Downloading latest Magisk APK..." - if ! curl -L -o "$magisk_apk" "$MAGISK_APK_URL"; then - error "Failed to download Magisk APK" - return 1 - fi - - log "Magisk downloaded successfully: $magisk_apk" - return 0 -} - -install_mtkclient() { - print_header "Installing MTKClient" - - local mtk_dir="${WORK_DIR}/mtkclient" - - if [[ -d $mtk_dir && -f "$mtk_dir/mtk.py" ]]; then - log "MTKClient already installed at $mtk_dir" - return 0 - fi - - if ! confirm "Install MTKClient for MediaTek boot image extraction?"; then - return 1 - fi - - log "Cloning MTKClient repository..." - if ! git clone https://github.com/bkerler/mtkclient "$mtk_dir"; then - error "Failed to clone MTKClient" - return 1 - fi - - log "Installing MTKClient Python dependencies..." - cd "$mtk_dir" - python3 -m pip install --user -r requirements.txt || warn "Some dependencies may have failed to install" - python3 -m pip install --user . || warn "MTKClient installation may be incomplete" - cd - > /dev/null - - log "MTKClient installed successfully" - return 0 -} - -extract_boot_with_mtkclient() { - print_header "Extracting Boot with MTKClient" - - local mtk_dir="${WORK_DIR}/mtkclient" - local boot_img="$WORK_DIR/boot.img" - local boot_a_img="$WORK_DIR/boot_a.img" - local vbmeta_a_img="$WORK_DIR/vbmeta_a.img" - - if [[ ! -d $mtk_dir ]]; then - error "MTKClient not installed. Run: $SCRIPT_NAME install-mtk" - return 1 - fi - - echo - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " MTKClient Boot ROM Mode Instructions" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo - echo "BEFORE continuing, you MUST:" - echo - echo " 1. Power off your phone COMPLETELY" - echo " 2. DISCONNECT the USB cable from your phone" - echo " 3. Have the USB cable ready in your hand" - echo - echo "When you press Enter:" - echo - echo " 4. Press and hold BOTH Volume buttons (Up + Down)" - echo " 5. While holding BOTH buttons, connect USB cable" - echo " 6. Keep holding until device is detected (may take 5-10 seconds)" - echo - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo - - if ! confirm "Phone is OFF and USB DISCONNECTED - ready to proceed?"; then - return 1 - fi - - log "Starting MTKClient NOW - quickly enter BROM mode!" - log "Hold BOTH volume buttons and connect USB cable..." - echo - - cd "$mtk_dir" - - # Activate venv and extract boot and vbmeta - if [[ ! -d "$mtk_dir/venv" ]]; then - error "MTKClient virtual environment not found. Run: $SCRIPT_NAME install-mtk" - cd - > /dev/null - return 1 - fi - - # shellcheck source=/dev/null - source venv/bin/activate - # BL9000 uses A/B partitions, so extract boot_a and vbmeta_a - if python3 mtk.py r boot_a,vbmeta_a "$boot_a_img,$vbmeta_a_img"; then - log "Boot_a and vbmeta_a extracted successfully!" - - # Copy boot_a.img to boot.img for Magisk compatibility - cp "$boot_a_img" "$boot_img" - BOOT_IMG="$boot_img" - - log "Boot image ready for patching: $BOOT_IMG" - - # Reset device - log "Resetting device..." - python3 mtk.py reset || warn "Failed to reset device, please reboot manually" - - # Deactivate venv if function exists - type deactivate &> /dev/null && deactivate - - cd - > /dev/null - return 0 - else - # Deactivate venv if function exists - type deactivate &> /dev/null && deactivate - - error "Failed to extract boot image with MTKClient" - cd - > /dev/null - return 1 - fi -} - -extract_boot_image() { - print_header "Extracting Boot Image" - - local boot_img="$WORK_DIR/boot.img" - - if [[ -n ${BOOT_IMG:-} && -f $BOOT_IMG ]]; then - log "Using provided boot image: $BOOT_IMG" - cp "$BOOT_IMG" "$boot_img" - BOOT_IMG="$boot_img" - return 0 - fi - - log "Attempting to extract boot image from device..." - - # Method 1: Try MTKClient first (best for MediaTek devices) - if [[ -d "${WORK_DIR}/mtkclient" ]]; then - log "Trying MTKClient extraction..." - if extract_boot_with_mtkclient; then - return 0 - fi - warn "MTKClient extraction failed, trying ADB methods..." - fi - - # Method 2: Try to pull boot partition directly via ADB - local boot_partition - boot_partition=$(adb shell "find /dev/block -name boot | head -n1" 2> /dev/null | tr -d '\r\n' || echo "") - - if [[ -n $boot_partition ]]; then - log "Found boot partition: $boot_partition" - if adb pull "$boot_partition" "$boot_img" 2> /dev/null; then - log "Boot image extracted successfully" - BOOT_IMG="$boot_img" - return 0 - fi - fi - - # Method 3: Try to get boot partition via by-name - boot_partition=$(adb shell "ls /dev/block/by-name/boot*" 2> /dev/null | head -n1 | tr -d '\r\n' || echo "") - - if [[ -n $boot_partition ]]; then - log "Found boot partition: $boot_partition" - if adb shell "su -c 'dd if=$boot_partition of=/sdcard/boot.img'" 2> /dev/null && - adb pull /sdcard/boot.img "$boot_img" 2> /dev/null; then - adb shell rm /sdcard/boot.img 2> /dev/null || true - log "Boot image extracted successfully" - BOOT_IMG="$boot_img" - return 0 - fi - fi - - error "Failed to extract boot image automatically." - echo - echo "Manual extraction options:" - echo "1. Use MTKClient: $SCRIPT_NAME extract-mtk" - echo "2. Extract boot.img from your device's firmware package" - echo "3. Get boot.img from device manufacturer's official ROM" - echo - echo "Then run: $SCRIPT_NAME root --boot-img /path/to/boot.img" - echo - - return 1 -} - -patch_boot_with_magisk() { - print_header "Patching Boot Image with Magisk" - - if [[ ! -f ${BOOT_IMG:-} ]]; then - die "Boot image not found: ${BOOT_IMG:-none}" - fi - - local magisk_apk="$WORK_DIR/magisk.apk" - if [[ ! -f $magisk_apk ]]; then - die "Magisk APK not found. Run download step first." - fi - - log "Checking if device is connected..." - if ! adb devices | grep -q "device$"; then - die "No device detected. Make sure USB debugging is enabled and device is connected." - fi - - log "Installing Magisk APK on device..." - if ! adb install -r "$magisk_apk" 2> /dev/null; then - warn "Magisk APK installation failed (may already be installed)" - fi - - log "Pushing boot image to device..." - adb push "$BOOT_IMG" /sdcard/Download/boot.img || die "Failed to push boot image" - - echo - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " MANUAL STEP REQUIRED" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo - echo "On your phone:" - echo "1. Open the Magisk app" - echo "2. Tap 'Install' next to Magisk" - echo "3. Select 'Select and Patch a File'" - echo "4. Navigate to Downloads and select boot.img" - echo "5. Tap 'Let's Go' and wait for patching to complete" - echo "6. The patched file will be saved as magisk_patched_*.img" - echo - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo - - if ! confirm "Have you completed patching the boot image in Magisk app?"; then - error "Patching cancelled by user" - return 1 - fi - - log "Pulling patched boot image from device..." - - local patched_img - patched_img=$(adb shell "ls /sdcard/Download/magisk_patched_*.img 2>/dev/null" | tr -d '\r\n' | head -n1 || echo "") - - if [[ -z $patched_img ]]; then - error "Could not find patched boot image on device." - echo "Please ensure the patching completed successfully in Magisk app." - return 1 - fi - - PATCHED_BOOT_IMG="$WORK_DIR/magisk_patched.img" - if ! adb pull "$patched_img" "$PATCHED_BOOT_IMG"; then - error "Failed to pull patched boot image" - return 1 - fi - - log "Patched boot image saved to: $PATCHED_BOOT_IMG" - return 0 -} - -flash_patched_boot() { - print_header "Flashing Patched Boot Image" - - if [[ ! -f ${PATCHED_BOOT_IMG:-} ]]; then - die "Patched boot image not found: ${PATCHED_BOOT_IMG:-none}" - fi - - echo - echo -e "${YELLOW}This will flash the patched boot image to your device.${NC}" - echo "Device uses A/B partitions - will flash to boot_a" - echo - - if ! confirm "Proceed with flashing?"; then - log "Flashing cancelled by user" - return 1 - fi - - log "Rebooting to bootloader..." - adb reboot bootloader || die "Failed to reboot to bootloader" - - log "Waiting for fastboot mode..." - sleep 5 - - if ! sudo fastboot devices | grep -q .; then - die "Device not detected in fastboot mode" - fi - - log "Flashing patched boot image to boot_a..." - if ! sudo fastboot flash boot_a "$PATCHED_BOOT_IMG"; then - error "Failed to flash boot image" - return 1 - fi - - log "Flashed successfully!" - log "Rebooting device..." - sudo fastboot reboot - - log "Waiting for device to boot..." - sleep 10 - adb wait-for-device || true - - echo - echo -e "${GREEN}╔═══════════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ Root Process Complete! ║${NC}" - echo -e "${GREEN}╚═══════════════════════════════════════════════════════╝${NC}" - echo - echo "Your BL9000 phone should now be rooted with Magisk!" - echo - echo "Next steps:" - echo "1. Open the Magisk app on your phone" - echo "2. Verify that it shows 'Installed' for both Magisk and App" - echo "3. Grant root access to apps as needed" - echo "4. Install Magisk modules if desired" - echo - echo "Note: Some banking and secure apps may not work with root." - echo " Use Magisk's DenyList feature to hide root from specific apps." - echo - - return 0 -} - -clean_work_dir() { - if [[ -d $WORK_DIR ]]; then - log "Removing working directory: $WORK_DIR" - rm -rf "$WORK_DIR" - log "Cleaned successfully" - else - log "Work directory doesn't exist: $WORK_DIR" - fi -} - -run_full_process() { - print_header "BL9000 Full Root Process" - - log "Starting complete rooting process..." - - install_dependencies || die "Failed to install dependencies" - setup_udev_rules || true - - echo - if ! confirm "Continue to device check?"; then - die "Process cancelled by user" - fi - - check_device || die "Device check failed" - - echo - if ! confirm "Continue to backup device data?"; then - die "Process cancelled by user" - fi - - backup_device_data || warn "Backup failed or incomplete" - - echo - if ! confirm "Continue to bootloader unlock?"; then - die "Process cancelled by user" - fi - - unlock_bootloader || die "Bootloader unlock failed" - - echo - log "Please complete device setup and re-enable USB debugging, then press Enter..." - read -r - - check_device || die "Device check failed after unlock" - - download_magisk || die "Failed to download Magisk" - extract_boot_image || die "Failed to extract boot image" - patch_boot_with_magisk || die "Failed to patch boot image" - flash_patched_boot || die "Failed to flash patched boot" - - log "Full root process completed successfully!" -} - -run_root_only() { - print_header "BL9000 Root Process (Skip Unlock)" - - log "Starting root process (assuming bootloader is already unlocked)..." - - check_device || die "Device check failed" - download_magisk || die "Failed to download Magisk" - extract_boot_image || die "Failed to extract boot image" - patch_boot_with_magisk || die "Failed to patch boot image" - flash_patched_boot || die "Failed to flash patched boot" - - log "Root process completed successfully!" -} +SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" +# shellcheck source=lib/bl9000_usage.sh +source "$SCRIPT_DIR/lib/bl9000_usage.sh" +# shellcheck source=lib/bl9000_deps.sh +source "$SCRIPT_DIR/lib/bl9000_deps.sh" +# shellcheck source=lib/bl9000_backup.sh +source "$SCRIPT_DIR/lib/bl9000_backup.sh" +# shellcheck source=lib/bl9000_device.sh +source "$SCRIPT_DIR/lib/bl9000_device.sh" +# shellcheck source=lib/bl9000_mtkclient.sh +source "$SCRIPT_DIR/lib/bl9000_mtkclient.sh" +# shellcheck source=lib/bl9000_magisk.sh +source "$SCRIPT_DIR/lib/bl9000_magisk.sh" main() { require_non_root