#!/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 < --destination ${APP_NAME} import --profile [--dry-run] [--verbose] ${APP_NAME} import --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 " exit 1 fi if [[ -z "$destination" ]]; then log_message "ERROR" "Use --destination " 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 , --uuid or --all" exit 1 fi ;; *) log_message "ERROR" "Unknown command: $command" usage exit 1 ;; esac } main "$@"