MediaImporter / RPI / scripts / rpi-camera-importer.sh
1 contributor
676 lines | 16.18kb
#!/usr/bin/env bash

set -euo pipefail

VERSION="0.2.0"
APP_NAME="rpi-camera-importer"
PROJECT_DIR="/usr/local/lib/rpi-camera-importer"
MEDIA_IMPORTER_SCRIPT="${PROJECT_DIR}/autonas-media-importer.sh"
CONFIG_DIR="${RPI_CAMERA_IMPORTER_CONFIG_DIR:-/etc/rpi-camera-importer}"
CONFIG_FILE="${CONFIG_DIR}/cameras.conf"
MOUNT_BASE="${RPI_CAMERA_IMPORTER_MOUNT_BASE:-/mnt/rpi-camera-importer}"

DRY_RUN=0
VERBOSE=0

log_message() {
  local level="$1"
  local message="$2"
  local ts
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
  echo "[$ts] [$level] $message"
}

usage() {
  cat <<EOF
${APP_NAME} v${VERSION}

Usage:
  ${APP_NAME} wizard
  ${APP_NAME} list
  ${APP_NAME} discover
  ${APP_NAME} verify
  ${APP_NAME} update --profile <name> --destination <path>
  ${APP_NAME} import --profile <name> [--dry-run] [--verbose]
  ${APP_NAME} import --uuid <uuid> [--dry-run] [--verbose]
  ${APP_NAME} import --all [--dry-run] [--verbose]

Config format:
  name|uuid|destination_path

Notes:
  - Import flow is device-trigger compatible (udev + systemd)
  - Media importer uses the proven AutoNAS importer script unchanged
EOF
}

require_root() {
  if [[ "${EUID}" -ne 0 ]]; then
    log_message "ERROR" "Run as root"
    exit 1
  fi
}

require_dependency() {
  local bin="$1"
  if ! command -v "$bin" >/dev/null 2>&1; then
    log_message "ERROR" "Missing dependency: $bin"
    exit 1
  fi
}

suggest_package_for_bin() {
  local bin="$1"
  case "$bin" in
    exiftool) echo "libimage-exiftool-perl" ;;
    blkid|mount|umount|mountpoint) echo "util-linux" ;;
    find) echo "findutils" ;;
    awk) echo "mawk" ;;
    sed|grep) echo "sed grep" ;;
    logger) echo "bsdutils" ;;
    *) echo "" ;;
  esac
}

ensure_runtime_dependencies() {
  local missing_bins=()
  local packages=()
  local dep pkg

  for dep in "$@"; do
    if ! command -v "$dep" >/dev/null 2>&1; then
      missing_bins+=("$dep")
      pkg="$(suggest_package_for_bin "$dep")"
      if [[ -n "$pkg" ]]; then
        for p in $pkg; do
          packages+=("$p")
        done
      fi
    fi
  done

  if [[ ${#missing_bins[@]} -eq 0 ]]; then
    return 0
  fi

  log_message "ERROR" "Missing runtime dependencies: ${missing_bins[*]}"

  if [[ ${#packages[@]} -gt 0 ]]; then
    local unique_packages
    unique_packages="$(printf '%s\n' "${packages[@]}" | awk '!seen[$0]++' | xargs)"
    log_message "ERROR" "Suggested install command: apt update && apt install -y ${unique_packages}"

    if [[ -t 0 && -t 1 ]] && command -v apt >/dev/null 2>&1; then
      local answer
      read -r -p "Install missing dependencies now? [y/N]: " answer
      if [[ "${answer,,}" == "y" || "${answer,,}" == "yes" ]]; then
        apt update
        apt install -y ${unique_packages}
      fi
    fi
  fi

  for dep in "$@"; do
    if ! command -v "$dep" >/dev/null 2>&1; then
      log_message "ERROR" "Dependency still missing after check: $dep"
      return 1
    fi
  done

  return 0
}

ensure_config_file() {
  mkdir -p "$CONFIG_DIR"
  if [[ ! -f "$CONFIG_FILE" ]]; then
    cat > "$CONFIG_FILE" <<'EOF'
# Camera profiles
# Format: name|uuid|destination_path
# Example: varia_rct715|A1B2-C3D4|/srv/media/varia
EOF
  fi

  # Normalize legacy entries to the current 3-field profile schema.
  : > "${CONFIG_FILE}.tmp"
  local line
  while IFS= read -r line; do
    if [[ -z "$line" || "$line" == \#* ]]; then
      echo "$line" >> "${CONFIG_FILE}.tmp"
      continue
    fi

    IFS='|' read -r name uuid destination _ <<< "$line"
    if [[ -n "$name" && -n "$uuid" && -n "$destination" ]]; then
      echo "${name}|${uuid}|${destination}" >> "${CONFIG_FILE}.tmp"
    fi
  done < "$CONFIG_FILE"
  mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
}

sanitize_profile_name() {
  local v="$1"
  echo "$v" | tr -cd '[:alnum:]_-' | xargs
}

profile_exists_by_name() {
  local profile="$1"
  grep -E "^${profile//./\\.}\|" "$CONFIG_FILE" >/dev/null 2>&1
}

profile_exists_by_uuid() {
  local uuid="$1"
  grep -E "^[^|]+\|${uuid//./\\.}\|" "$CONFIG_FILE" >/dev/null 2>&1
}

find_profile_line() {
  local mode="$1"
  local target="$2"
  local line

  while IFS= read -r line; do
    [[ -z "$line" || "$line" == \#* ]] && continue
    IFS='|' read -r name uuid destination _ <<< "$line"

    case "$mode" in
      name)
        if [[ "$name" == "$target" ]]; then
          echo "$line"
          return 0
        fi
        ;;
      uuid)
        if [[ "$uuid" == "$target" ]]; then
          echo "$line"
          return 0
        fi
        ;;
      *)
        return 1
        ;;
    esac
  done < "$CONFIG_FILE"

  return 1
}

list_profiles() {
  ensure_config_file
  echo ""
  echo "Configured camera profiles"
  echo "--------------------------------------------------------------------------"
  printf "%-20s %-16s %-30s\n" "Name" "UUID" "Destination"
  echo "--------------------------------------------------------------------------"

  local line
  while IFS= read -r line; do
    [[ -z "$line" || "$line" == \#* ]] && continue
    IFS='|' read -r name uuid destination _ <<< "$line"
    printf "%-20s %-16s %-30s\n" "$name" "$uuid" "$destination"
  done < "$CONFIG_FILE"

  echo "--------------------------------------------------------------------------"
}

discover_devices() {
  ensure_runtime_dependencies "blkid" || return 1
  echo ""
  echo "Detected removable/storage block devices"
  echo "---------------------------------------------------------------"
  blkid -o export 2>/dev/null | awk '
    /^DEVNAME=/ {dev=$0; sub(/^DEVNAME=/, "", dev)}
    /^UUID=/ {uuid=$0; sub(/^UUID=/, "", uuid)}
    /^TYPE=/ {type=$0; sub(/^TYPE=/, "", type)}
    /^$/ {
      if (dev != "" && uuid != "") {
        printf "%s | UUID=%s | FS=%s\n", dev, uuid, type
      }
      dev=""; uuid=""; type=""
    }
    END {
      if (dev != "" && uuid != "") {
        printf "%s | UUID=%s | FS=%s\n", dev, uuid, type
      }
    }
  '
  echo "---------------------------------------------------------------"
}

add_profile_interactive() {
  ensure_config_file

  echo ""
  echo "Tip: conecteaza camera si ruleaza optional 'discover' intr-un alt terminal."

  local name uuid destination
  read -r -p "Profile name (ex: varia_rct715): " name
  name="$(sanitize_profile_name "$name")"
  if [[ -z "$name" ]]; then
    echo "Invalid profile name"
    return
  fi

  if profile_exists_by_name "$name"; then
    echo "Profile already exists"
    return
  fi

  read -r -p "Camera UUID (ID_FS_UUID): " uuid
  if [[ -z "$uuid" ]]; then
    echo "UUID is required"
    return
  fi

  if profile_exists_by_uuid "$uuid"; then
    echo "UUID already configured"
    return
  fi

  read -r -p "Destination path (ex: /srv/media/varia): " destination
  if [[ -z "$destination" ]]; then
    echo "Destination is required"
    return
  fi

  echo "${name}|${uuid}|${destination}" >> "$CONFIG_FILE"
  echo "Profile '$name' added"
}

remove_profile_interactive() {
  ensure_config_file
  local name
  read -r -p "Profile name to remove: " name
  name="$(sanitize_profile_name "$name")"

  if ! profile_exists_by_name "$name"; then
    echo "Profile not found"
    return
  fi

  grep -v -E "^${name//./\\.}\|" "$CONFIG_FILE" > "${CONFIG_FILE}.tmp"
  mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
  echo "Profile '$name' removed"
}

update_profile_values() {
  local target_name="$1"
  local new_destination="$2"

  if ! profile_exists_by_name "$target_name"; then
    log_message "ERROR" "Profile '$target_name' not found"
    return 1
  fi

  : > "${CONFIG_FILE}.tmp"
  local line
  local changed=0
  while IFS= read -r line; do
    if [[ -z "$line" || "$line" == \#* ]]; then
      echo "$line" >> "${CONFIG_FILE}.tmp"
      continue
    fi

    IFS='|' read -r name uuid destination _ <<< "$line"
    if [[ "$name" == "$target_name" ]]; then
      [[ -n "$new_destination" ]] && destination="$new_destination"
      changed=1
    fi

    echo "${name}|${uuid}|${destination}" >> "${CONFIG_FILE}.tmp"
  done < "$CONFIG_FILE"

  mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"

  if [[ $changed -eq 1 ]]; then
    log_message "INFO" "Profile '$target_name' updated"
  fi
}

update_profile_interactive() {
  ensure_config_file

  local name
  read -r -p "Profile name to update: " name
  name="$(sanitize_profile_name "$name")"

  local line
  if ! line="$(find_profile_line "name" "$name")"; then
    echo "Profile not found"
    return 1
  fi

  local current_name current_uuid current_destination
  IFS='|' read -r current_name current_uuid current_destination _ <<< "$line"

  echo "Current UUID: $current_uuid"
  echo "Current destination: $current_destination"

  local new_destination
  read -r -p "New destination path (leave blank to keep current): " new_destination

  update_profile_values "$name" "$new_destination"
}

run_import_for_entry() {
  local name="$1"
  local uuid="$2"
  local destination="$3"

  local mount_point="${MOUNT_BASE}/${name}"
  local device_path="/dev/disk/by-uuid/${uuid}"

  if [[ ! -x "$MEDIA_IMPORTER_SCRIPT" ]]; then
    log_message "ERROR" "Media importer missing: $MEDIA_IMPORTER_SCRIPT"
    return 1
  fi

  if [[ ! -e "$device_path" ]]; then
    log_message "WARN" "Device for UUID $uuid not present, skipping profile '$name'"
    return 0
  fi

  mkdir -p "$mount_point" "$destination"

  local mounted_here=0
  if ! mountpoint -q "$mount_point" 2>/dev/null; then
    if [[ $DRY_RUN -eq 1 ]]; then
      log_message "INFO" "[dry-run] would mount $device_path at $mount_point"
    else
      mount "$device_path" "$mount_point"
      mounted_here=1
    fi
  fi

  local args=("$mount_point" "$destination")
  if [[ $DRY_RUN -eq 1 ]]; then
    args+=("--dry-run")
  fi
  if [[ $VERBOSE -eq 1 ]]; then
    args+=("--verbose")
  fi

  log_message "INFO" "Import start for '$name' (uuid=$uuid)"
  if [[ $DRY_RUN -eq 1 ]]; then
    "$MEDIA_IMPORTER_SCRIPT" "${args[@]}" || true
  else
    "$MEDIA_IMPORTER_SCRIPT" "${args[@]}"
  fi
  local importer_rc=$?

  if [[ $mounted_here -eq 1 ]]; then
    sync || true
    umount "$mount_point" || log_message "WARN" "Could not unmount $mount_point"
  fi

  if [[ $importer_rc -ne 0 ]]; then
    log_message "ERROR" "Import failed for '$name'"
    return 1
  fi

  log_message "INFO" "Import finished for '$name'"
  return 0
}

import_profile_by_name() {
  ensure_config_file
  local target="$1"
  local line
  if ! line="$(find_profile_line "name" "$target")"; then
    log_message "ERROR" "Profile '$target' not found"
    return 1
  fi

  local name uuid destination
  IFS='|' read -r name uuid destination _ <<< "$line"
  run_import_for_entry "$name" "$uuid" "$destination"
}

import_profile_by_uuid() {
  ensure_config_file
  local target_uuid="$1"

  local line
  if ! line="$(find_profile_line "uuid" "$target_uuid")"; then
    log_message "INFO" "UUID '$target_uuid' is not configured"
    return 0
  fi

  local name uuid destination
  IFS='|' read -r name uuid destination _ <<< "$line"
  run_import_for_entry "$name" "$uuid" "$destination"
}

verify_pipeline() {
  ensure_config_file

  local rc=0
  local deps=(mount umount mountpoint exiftool blkid find awk sed grep logger)
  local dep

  log_message "INFO" "Verifying dependencies"
  for dep in "${deps[@]}"; do
    if command -v "$dep" >/dev/null 2>&1; then
      log_message "INFO" "OK dependency: $dep"
    else
      log_message "ERROR" "Missing dependency: $dep"
      rc=1
    fi
  done

  local paths=(
    "$MEDIA_IMPORTER_SCRIPT"
    "/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh"
    "/usr/local/lib/rpi-camera-importer/rpi-camera-udev-wrapper.sh"
    "/etc/udev/rules.d/99-rpi-camera-importer.rules"
    "/etc/systemd/system/rpi-camera-importer-attach@.service"
  )
  local p

  log_message "INFO" "Verifying runtime files"
  for p in "${paths[@]}"; do
    if [[ -e "$p" ]]; then
      log_message "INFO" "OK file: $p"
    else
      log_message "ERROR" "Missing file: $p"
      rc=1
    fi
  done

  if command -v systemctl >/dev/null 2>&1; then
    if systemctl daemon-reload >/dev/null 2>&1; then
      log_message "INFO" "OK systemd daemon-reload"
    else
      log_message "WARN" "systemd daemon-reload returned non-zero"
    fi
  fi

  log_message "INFO" "Configured profiles overview"
  list_profiles

  log_message "INFO" "Connected block devices overview"
  discover_devices

  if [[ $rc -eq 0 ]]; then
    log_message "INFO" "Mechanism verification completed successfully"
  else
    log_message "ERROR" "Mechanism verification failed"
  fi

  return $rc
}

import_all_profiles() {
  ensure_config_file
  local line any=0

  while IFS= read -r line; do
    [[ -z "$line" || "$line" == \#* ]] && continue
    IFS='|' read -r name uuid destination _ <<< "$line"

    any=1
    run_import_for_entry "$name" "$uuid" "$destination"
  done < "$CONFIG_FILE"

  if [[ $any -eq 0 ]]; then
    log_message "WARN" "No profiles found"
  fi
}

wizard() {
  ensure_config_file

  while true; do
    echo ""
    echo "=== Raspberry Pi Camera Importer Wizard ==="
    echo "1) List profiles"
    echo "2) Discover connected devices (UUID)"
    echo "3) Add profile"
    echo "4) Remove profile"
    echo "5) Update profile destination"
    echo "6) Run import for one profile"
    echo "7) Run import for all profiles"
    echo "0) Exit"
    echo ""

    local choice
    read -r -p "Choose option: " choice

    case "$choice" in
      1) list_profiles ;;
      2) discover_devices ;;
      3) add_profile_interactive ;;
      4) remove_profile_interactive ;;
      5) update_profile_interactive ;;
      6)
        local p
        read -r -p "Profile name: " p
        import_profile_by_name "$(sanitize_profile_name "$p")"
        ;;
      7) import_all_profiles ;;
      0) break ;;
      *) echo "Invalid option" ;;
    esac
  done
}

main() {
  local command="${1:-}"
  if [[ -z "$command" ]]; then
    usage
    exit 1
  fi
  shift || true

  case "$command" in
    --help|-h)
      usage
      ;;
    list)
      require_root
      list_profiles
      ;;
    discover)
      require_root
      ensure_runtime_dependencies "blkid" || exit 1
      discover_devices
      ;;
    verify)
      require_root
      verify_pipeline
      ;;
    update)
      require_root
      ensure_config_file

      local profile=""
      local destination=""

      while [[ $# -gt 0 ]]; do
        case "$1" in
          --profile)
            profile="$2"
            shift 2
            ;;
          --destination)
            destination="$2"
            shift 2
            ;;
          *)
            log_message "ERROR" "Unknown option: $1"
            exit 1
            ;;
        esac
      done

      if [[ -z "$profile" ]]; then
        log_message "ERROR" "Use --profile <name>"
        exit 1
      fi

      if [[ -z "$destination" ]]; then
        log_message "ERROR" "Use --destination <path>"
        exit 1
      fi

      update_profile_values "$profile" "$destination"
      ;;
    wizard)
      require_root
      ensure_runtime_dependencies "mount" "umount" "mountpoint" "exiftool" "blkid" || exit 1
      wizard
      ;;
    import)
      require_root
      ensure_runtime_dependencies "mount" "umount" "mountpoint" "exiftool" || exit 1
      local profile=""
      local uuid=""
      local import_all=0

      while [[ $# -gt 0 ]]; do
        case "$1" in
          --profile)
            profile="$2"
            shift 2
            ;;
          --uuid)
            uuid="$2"
            shift 2
            ;;
          --all)
            import_all=1
            shift
            ;;
          --dry-run)
            DRY_RUN=1
            shift
            ;;
          --verbose)
            VERBOSE=1
            shift
            ;;
          *)
            log_message "ERROR" "Unknown option: $1"
            exit 1
            ;;
        esac
      done

      if [[ $import_all -eq 1 ]]; then
        import_all_profiles
      elif [[ -n "$profile" ]]; then
        import_profile_by_name "$profile"
      elif [[ -n "$uuid" ]]; then
        import_profile_by_uuid "$uuid"
      else
        log_message "ERROR" "Use --profile <name>, --uuid <uuid> or --all"
        exit 1
      fi
      ;;
    *)
      log_message "ERROR" "Unknown command: $command"
      usage
      exit 1
      ;;
  esac
}

main "$@"