1 contributor
#!/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 "$@"