MediaImporter / RPI / scripts / rpi-camera-importer.sh
Newer Older
676 lines | 16.18kb
Bogdan Timofte authored 3 weeks ago
1
#!/usr/bin/env bash
2

            
3
set -euo pipefail
4

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

            
13
DRY_RUN=0
14
VERBOSE=0
15

            
16
log_message() {
17
  local level="$1"
18
  local message="$2"
19
  local ts
20
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
21
  echo "[$ts] [$level] $message"
22
}
23

            
24
usage() {
25
  cat <<EOF
26
${APP_NAME} v${VERSION}
27

            
28
Usage:
29
  ${APP_NAME} wizard
30
  ${APP_NAME} list
31
  ${APP_NAME} discover
32
  ${APP_NAME} verify
33
  ${APP_NAME} update --profile <name> --destination <path>
34
  ${APP_NAME} import --profile <name> [--dry-run] [--verbose]
35
  ${APP_NAME} import --uuid <uuid> [--dry-run] [--verbose]
36
  ${APP_NAME} import --all [--dry-run] [--verbose]
37

            
38
Config format:
39
  name|uuid|destination_path
40

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

            
47
require_root() {
48
  if [[ "${EUID}" -ne 0 ]]; then
49
    log_message "ERROR" "Run as root"
50
    exit 1
51
  fi
52
}
53

            
54
require_dependency() {
55
  local bin="$1"
56
  if ! command -v "$bin" >/dev/null 2>&1; then
57
    log_message "ERROR" "Missing dependency: $bin"
58
    exit 1
59
  fi
60
}
61

            
62
suggest_package_for_bin() {
63
  local bin="$1"
64
  case "$bin" in
65
    exiftool) echo "libimage-exiftool-perl" ;;
66
    blkid|mount|umount|mountpoint) echo "util-linux" ;;
67
    find) echo "findutils" ;;
68
    awk) echo "mawk" ;;
69
    sed|grep) echo "sed grep" ;;
70
    logger) echo "bsdutils" ;;
71
    *) echo "" ;;
72
  esac
73
}
74

            
75
ensure_runtime_dependencies() {
76
  local missing_bins=()
77
  local packages=()
78
  local dep pkg
79

            
80
  for dep in "$@"; do
81
    if ! command -v "$dep" >/dev/null 2>&1; then
82
      missing_bins+=("$dep")
83
      pkg="$(suggest_package_for_bin "$dep")"
84
      if [[ -n "$pkg" ]]; then
85
        for p in $pkg; do
86
          packages+=("$p")
87
        done
88
      fi
89
    fi
90
  done
91

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

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

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

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

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

            
120
  return 0
121
}
122

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

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

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

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

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

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

            
165
find_profile_line() {
166
  local mode="$1"
167
  local target="$2"
168
  local line
169

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

            
174
    case "$mode" in
175
      name)
176
        if [[ "$name" == "$target" ]]; then
177
          echo "$line"
178
          return 0
179
        fi
180
        ;;
181
      uuid)
182
        if [[ "$uuid" == "$target" ]]; then
183
          echo "$line"
184
          return 0
185
        fi
186
        ;;
187
      *)
188
        return 1
189
        ;;
190
    esac
191
  done < "$CONFIG_FILE"
192

            
193
  return 1
194
}
195

            
196
list_profiles() {
197
  ensure_config_file
198
  echo ""
199
  echo "Configured camera profiles"
200
  echo "--------------------------------------------------------------------------"
201
  printf "%-20s %-16s %-30s\n" "Name" "UUID" "Destination"
202
  echo "--------------------------------------------------------------------------"
203

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

            
211
  echo "--------------------------------------------------------------------------"
212
}
213

            
214
discover_devices() {
215
  ensure_runtime_dependencies "blkid" || return 1
216
  echo ""
217
  echo "Detected removable/storage block devices"
218
  echo "---------------------------------------------------------------"
219
  blkid -o export 2>/dev/null | awk '
220
    /^DEVNAME=/ {dev=$0; sub(/^DEVNAME=/, "", dev)}
221
    /^UUID=/ {uuid=$0; sub(/^UUID=/, "", uuid)}
222
    /^TYPE=/ {type=$0; sub(/^TYPE=/, "", type)}
223
    /^$/ {
224
      if (dev != "" && uuid != "") {
225
        printf "%s | UUID=%s | FS=%s\n", dev, uuid, type
226
      }
227
      dev=""; uuid=""; type=""
228
    }
229
    END {
230
      if (dev != "" && uuid != "") {
231
        printf "%s | UUID=%s | FS=%s\n", dev, uuid, type
232
      }
233
    }
234
  '
235
  echo "---------------------------------------------------------------"
236
}
237

            
238
add_profile_interactive() {
239
  ensure_config_file
240

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

            
244
  local name uuid destination
245
  read -r -p "Profile name (ex: varia_rct715): " name
246
  name="$(sanitize_profile_name "$name")"
247
  if [[ -z "$name" ]]; then
248
    echo "Invalid profile name"
249
    return
250
  fi
251

            
252
  if profile_exists_by_name "$name"; then
253
    echo "Profile already exists"
254
    return
255
  fi
256

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

            
263
  if profile_exists_by_uuid "$uuid"; then
264
    echo "UUID already configured"
265
    return
266
  fi
267

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

            
274
  echo "${name}|${uuid}|${destination}" >> "$CONFIG_FILE"
275
  echo "Profile '$name' added"
276
}
277

            
278
remove_profile_interactive() {
279
  ensure_config_file
280
  local name
281
  read -r -p "Profile name to remove: " name
282
  name="$(sanitize_profile_name "$name")"
283

            
284
  if ! profile_exists_by_name "$name"; then
285
    echo "Profile not found"
286
    return
287
  fi
288

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

            
294
update_profile_values() {
295
  local target_name="$1"
296
  local new_destination="$2"
297

            
298
  if ! profile_exists_by_name "$target_name"; then
299
    log_message "ERROR" "Profile '$target_name' not found"
300
    return 1
301
  fi
302

            
303
  : > "${CONFIG_FILE}.tmp"
304
  local line
305
  local changed=0
306
  while IFS= read -r line; do
307
    if [[ -z "$line" || "$line" == \#* ]]; then
308
      echo "$line" >> "${CONFIG_FILE}.tmp"
309
      continue
310
    fi
311

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

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

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

            
323
  if [[ $changed -eq 1 ]]; then
324
    log_message "INFO" "Profile '$target_name' updated"
325
  fi
326
}
327

            
328
update_profile_interactive() {
329
  ensure_config_file
330

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

            
335
  local line
336
  if ! line="$(find_profile_line "name" "$name")"; then
337
    echo "Profile not found"
338
    return 1
339
  fi
340

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

            
344
  echo "Current UUID: $current_uuid"
345
  echo "Current destination: $current_destination"
346

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

            
350
  update_profile_values "$name" "$new_destination"
351
}
352

            
353
run_import_for_entry() {
354
  local name="$1"
355
  local uuid="$2"
356
  local destination="$3"
357

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

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

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

            
371
  mkdir -p "$mount_point" "$destination"
372

            
373
  local mounted_here=0
374
  if ! mountpoint -q "$mount_point" 2>/dev/null; then
375
    if [[ $DRY_RUN -eq 1 ]]; then
376
      log_message "INFO" "[dry-run] would mount $device_path at $mount_point"
377
    else
378
      mount "$device_path" "$mount_point"
379
      mounted_here=1
380
    fi
381
  fi
382

            
383
  local args=("$mount_point" "$destination")
384
  if [[ $DRY_RUN -eq 1 ]]; then
385
    args+=("--dry-run")
386
  fi
387
  if [[ $VERBOSE -eq 1 ]]; then
388
    args+=("--verbose")
389
  fi
390

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

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

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

            
409
  log_message "INFO" "Import finished for '$name'"
410
  return 0
411
}
412

            
413
import_profile_by_name() {
414
  ensure_config_file
415
  local target="$1"
416
  local line
417
  if ! line="$(find_profile_line "name" "$target")"; then
418
    log_message "ERROR" "Profile '$target' not found"
419
    return 1
420
  fi
421

            
422
  local name uuid destination
423
  IFS='|' read -r name uuid destination _ <<< "$line"
424
  run_import_for_entry "$name" "$uuid" "$destination"
425
}
426

            
427
import_profile_by_uuid() {
428
  ensure_config_file
429
  local target_uuid="$1"
430

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

            
437
  local name uuid destination
438
  IFS='|' read -r name uuid destination _ <<< "$line"
439
  run_import_for_entry "$name" "$uuid" "$destination"
440
}
441

            
442
verify_pipeline() {
443
  ensure_config_file
444

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

            
449
  log_message "INFO" "Verifying dependencies"
450
  for dep in "${deps[@]}"; do
451
    if command -v "$dep" >/dev/null 2>&1; then
452
      log_message "INFO" "OK dependency: $dep"
453
    else
454
      log_message "ERROR" "Missing dependency: $dep"
455
      rc=1
456
    fi
457
  done
458

            
459
  local paths=(
460
    "$MEDIA_IMPORTER_SCRIPT"
461
    "/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh"
462
    "/usr/local/lib/rpi-camera-importer/rpi-camera-udev-wrapper.sh"
463
    "/etc/udev/rules.d/99-rpi-camera-importer.rules"
464
    "/etc/systemd/system/rpi-camera-importer-attach@.service"
465
  )
466
  local p
467

            
468
  log_message "INFO" "Verifying runtime files"
469
  for p in "${paths[@]}"; do
470
    if [[ -e "$p" ]]; then
471
      log_message "INFO" "OK file: $p"
472
    else
473
      log_message "ERROR" "Missing file: $p"
474
      rc=1
475
    fi
476
  done
477

            
478
  if command -v systemctl >/dev/null 2>&1; then
479
    if systemctl daemon-reload >/dev/null 2>&1; then
480
      log_message "INFO" "OK systemd daemon-reload"
481
    else
482
      log_message "WARN" "systemd daemon-reload returned non-zero"
483
    fi
484
  fi
485

            
486
  log_message "INFO" "Configured profiles overview"
487
  list_profiles
488

            
489
  log_message "INFO" "Connected block devices overview"
490
  discover_devices
491

            
492
  if [[ $rc -eq 0 ]]; then
493
    log_message "INFO" "Mechanism verification completed successfully"
494
  else
495
    log_message "ERROR" "Mechanism verification failed"
496
  fi
497

            
498
  return $rc
499
}
500

            
501
import_all_profiles() {
502
  ensure_config_file
503
  local line any=0
504

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

            
509
    any=1
510
    run_import_for_entry "$name" "$uuid" "$destination"
511
  done < "$CONFIG_FILE"
512

            
513
  if [[ $any -eq 0 ]]; then
514
    log_message "WARN" "No profiles found"
515
  fi
516
}
517

            
518
wizard() {
519
  ensure_config_file
520

            
521
  while true; do
522
    echo ""
523
    echo "=== Raspberry Pi Camera Importer Wizard ==="
524
    echo "1) List profiles"
525
    echo "2) Discover connected devices (UUID)"
526
    echo "3) Add profile"
527
    echo "4) Remove profile"
528
    echo "5) Update profile destination"
529
    echo "6) Run import for one profile"
530
    echo "7) Run import for all profiles"
531
    echo "0) Exit"
532
    echo ""
533

            
534
    local choice
535
    read -r -p "Choose option: " choice
536

            
537
    case "$choice" in
538
      1) list_profiles ;;
539
      2) discover_devices ;;
540
      3) add_profile_interactive ;;
541
      4) remove_profile_interactive ;;
542
      5) update_profile_interactive ;;
543
      6)
544
        local p
545
        read -r -p "Profile name: " p
546
        import_profile_by_name "$(sanitize_profile_name "$p")"
547
        ;;
548
      7) import_all_profiles ;;
549
      0) break ;;
550
      *) echo "Invalid option" ;;
551
    esac
552
  done
553
}
554

            
555
main() {
556
  local command="${1:-}"
557
  if [[ -z "$command" ]]; then
558
    usage
559
    exit 1
560
  fi
561
  shift || true
562

            
563
  case "$command" in
564
    --help|-h)
565
      usage
566
      ;;
567
    list)
568
      require_root
569
      list_profiles
570
      ;;
571
    discover)
572
      require_root
573
      ensure_runtime_dependencies "blkid" || exit 1
574
      discover_devices
575
      ;;
576
    verify)
577
      require_root
578
      verify_pipeline
579
      ;;
580
    update)
581
      require_root
582
      ensure_config_file
583

            
584
      local profile=""
585
      local destination=""
586

            
587
      while [[ $# -gt 0 ]]; do
588
        case "$1" in
589
          --profile)
590
            profile="$2"
591
            shift 2
592
            ;;
593
          --destination)
594
            destination="$2"
595
            shift 2
596
            ;;
597
          *)
598
            log_message "ERROR" "Unknown option: $1"
599
            exit 1
600
            ;;
601
        esac
602
      done
603

            
604
      if [[ -z "$profile" ]]; then
605
        log_message "ERROR" "Use --profile <name>"
606
        exit 1
607
      fi
608

            
609
      if [[ -z "$destination" ]]; then
610
        log_message "ERROR" "Use --destination <path>"
611
        exit 1
612
      fi
613

            
614
      update_profile_values "$profile" "$destination"
615
      ;;
616
    wizard)
617
      require_root
618
      ensure_runtime_dependencies "mount" "umount" "mountpoint" "exiftool" "blkid" || exit 1
619
      wizard
620
      ;;
621
    import)
622
      require_root
623
      ensure_runtime_dependencies "mount" "umount" "mountpoint" "exiftool" || exit 1
624
      local profile=""
625
      local uuid=""
626
      local import_all=0
627

            
628
      while [[ $# -gt 0 ]]; do
629
        case "$1" in
630
          --profile)
631
            profile="$2"
632
            shift 2
633
            ;;
634
          --uuid)
635
            uuid="$2"
636
            shift 2
637
            ;;
638
          --all)
639
            import_all=1
640
            shift
641
            ;;
642
          --dry-run)
643
            DRY_RUN=1
644
            shift
645
            ;;
646
          --verbose)
647
            VERBOSE=1
648
            shift
649
            ;;
650
          *)
651
            log_message "ERROR" "Unknown option: $1"
652
            exit 1
653
            ;;
654
        esac
655
      done
656

            
657
      if [[ $import_all -eq 1 ]]; then
658
        import_all_profiles
659
      elif [[ -n "$profile" ]]; then
660
        import_profile_by_name "$profile"
661
      elif [[ -n "$uuid" ]]; then
662
        import_profile_by_uuid "$uuid"
663
      else
664
        log_message "ERROR" "Use --profile <name>, --uuid <uuid> or --all"
665
        exit 1
666
      fi
667
      ;;
668
    *)
669
      log_message "ERROR" "Unknown command: $command"
670
      usage
671
      exit 1
672
      ;;
673
  esac
674
}
675

            
676
main "$@"