@@ -1,3 +1,12 @@ |
||
| 1 | 1 |
# Exclude samples directory from version control |
| 2 | 2 |
sample/ |
| 3 | 3 |
tests/ |
| 4 |
+.DS_Store |
|
| 5 |
+test_reports/ |
|
| 6 |
+test/.DS_Store |
|
| 7 |
+test/*_before.txt |
|
| 8 |
+test/*_after.txt |
|
| 9 |
+test/import_log.txt |
|
| 10 |
+test/source/sorted/ |
|
| 11 |
+test/destination/* |
|
| 12 |
+!test/destination/.gitkeep |
|
@@ -2,10 +2,10 @@ |
||
| 2 | 2 |
|
| 3 | 3 |
This repository contains the development and testing resources for the `media-importer.sh` script, which is the main product of this project. |
| 4 | 4 |
|
| 5 |
- |
|
| 6 | 5 |
## Project Structure |
| 7 | 6 |
|
| 8 | 7 |
- `media-importer.sh`: The primary script under development and testing. |
| 8 |
+- `RPI/`: Raspberry Pi integration prototype. This area is currently frozen because Raspberry Pi 3B+ hardware with 1 GB RAM has not provided reliable validation capacity and may also be affected by hardware defects. |
|
| 9 | 9 |
- `samples/code/autonas-media-importer.sh`: The original script that serves as inspiration for this project. |
| 10 | 10 |
- `samples/`: Contains code samples, example files, and other resources used for development and testing. **Note:** Scripts and functions are not imported from this directory; it is strictly for reference and testing purposes. |
| 11 | 11 |
|
@@ -15,7 +15,7 @@ Version control is implemented for all files except the `samples/` directory. Th |
||
| 15 | 15 |
|
| 16 | 16 |
To exclude the `samples/` directory, the following entry should be added to the `.gitignore` file: |
| 17 | 17 |
|
| 18 |
-``` |
|
| 18 |
+```gitignore |
|
| 19 | 19 |
samples/ |
| 20 | 20 |
``` |
| 21 | 21 |
|
@@ -31,7 +31,6 @@ Specify your license here. |
||
| 31 | 31 |
|
| 32 | 32 |
--- |
| 33 | 33 |
|
| 34 |
- |
|
| 35 | 34 |
For details on recent changes, including the removal of fallback date handling, see [CHANGELOG.md](./CHANGELOG.md). |
| 36 | 35 |
|
| 37 | 36 |
This file is intended for GitHub Copilot and contributors to understand the project structure and guidelines. |
@@ -0,0 +1,15 @@ |
||
| 1 |
+# Changelog |
|
| 2 |
+ |
|
| 3 |
+## 0.2.0 - 2026-03-12 |
|
| 4 |
+ |
|
| 5 |
+- Switched configuration to UUID-based camera profiles for reliable attach detection. |
|
| 6 |
+- Added udev + wrapper + systemd attach pipeline modeled after proven AutoNAS behavior. |
|
| 7 |
+- Added disk handler to run imports on device detection events. |
|
| 8 |
+- Reused AutoNAS media importer script unchanged for proven Varia-compatible import behavior. |
|
| 9 |
+ |
|
| 10 |
+## 0.1.0 - 2026-03-12 |
|
| 11 |
+ |
|
| 12 |
+- Created standalone Raspberry Pi camera media importer project. |
|
| 13 |
+- Added interactive menu-based wizard for profile management. |
|
| 14 |
+- Added media import engine with EXIF date extraction and QuickTime UTC conversion. |
|
| 15 |
+- Added setup script for dependency and file installation. |
|
@@ -0,0 +1,46 @@ |
||
| 1 |
+# Install Guide |
|
| 2 |
+ |
|
| 3 |
+## Status |
|
| 4 |
+ |
|
| 5 |
+RPI section is frozen. |
|
| 6 |
+ |
|
| 7 |
+Nu se recomanda folosirea acestui ghid ca baza pentru testare curenta pe Raspberry Pi 3B+ cu 1 GB RAM. Platforma nu a oferit suficiente resurse pentru validare fiabila si exista posibilitatea unui defect hardware. |
|
| 8 |
+ |
|
| 9 |
+## 1. Copy project to Raspberry Pi |
|
| 10 |
+ |
|
| 11 |
+Clone or copy this folder to the Raspberry Pi. |
|
| 12 |
+ |
|
| 13 |
+## 2. Run installer |
|
| 14 |
+ |
|
| 15 |
+```bash |
|
| 16 |
+cd rpi-camera-media-importer |
|
| 17 |
+sudo ./setup.sh |
|
| 18 |
+``` |
|
| 19 |
+ |
|
| 20 |
+## 3. Configure camera profiles |
|
| 21 |
+ |
|
| 22 |
+```bash |
|
| 23 |
+rpi-camera-importer wizard |
|
| 24 |
+``` |
|
| 25 |
+ |
|
| 26 |
+In wizard, configure each camera by `ID_FS_UUID` and destination path. |
|
| 27 |
+ |
|
| 28 |
+## 4. Verify detection pipeline (udev + systemd) |
|
| 29 |
+ |
|
| 30 |
+Connect camera and verify attach unit logs: |
|
| 31 |
+ |
|
| 32 |
+```bash |
|
| 33 |
+journalctl -u rpi-camera-importer-attach@<UUID>.service -f |
|
| 34 |
+``` |
|
| 35 |
+ |
|
| 36 |
+## 5. Test import |
|
| 37 |
+ |
|
| 38 |
+```bash |
|
| 39 |
+rpi-camera-importer import --all --dry-run --verbose |
|
| 40 |
+``` |
|
| 41 |
+ |
|
| 42 |
+## 6. Run real import |
|
| 43 |
+ |
|
| 44 |
+```bash |
|
| 45 |
+rpi-camera-importer import --all |
|
| 46 |
+``` |
|
@@ -0,0 +1,103 @@ |
||
| 1 |
+# Raspberry Pi Camera Media Importer |
|
| 2 |
+ |
|
| 3 |
+## Status |
|
| 4 |
+ |
|
| 5 |
+Aceasta sectiune este frozen. |
|
| 6 |
+ |
|
| 7 |
+Motiv: Raspberry Pi 3B+ cu 1 GB RAM nu ofera suficiente resurse pentru testare si validare fiabila, iar comportamentul observat poate indica si un defect hardware. Pana la confirmarea unei platforme mai stabile, componenta RPI ramane documentata, dar nu mai este considerata activ dezvoltata sau validata. |
|
| 8 |
+ |
|
| 9 |
+Proiect standalone pentru Raspberry Pi care importa media de pe camere (foto/video) prin detectie automata la conectare dispozitiv. |
|
| 10 |
+ |
|
| 11 |
+Acest proiect este independent de cluster si nu are integrare cu codul de deploy existent. |
|
| 12 |
+ |
|
| 13 |
+## Cerinte cheie implementate |
|
| 14 |
+ |
|
| 15 |
+- Compatibilitate cu camere Garmin Varia. |
|
| 16 |
+- Importerul media existent, testat in AutoNAS, este reutilizat fara modificari in acest proiect. |
|
| 17 |
+- Trigger la detectie dispozitiv, pe mecanismul testat AutoNAS: |
|
| 18 |
+ - udev rule |
|
| 19 |
+ - wrapper (pentru device-uri ata bridge) |
|
| 20 |
+ - systemd attach template unit |
|
| 21 |
+ - disk handler care ruleaza import pe UUID |
|
| 22 |
+ |
|
| 23 |
+## Functionalitati |
|
| 24 |
+ |
|
| 25 |
+- Wizard menu-based pentru configurare profile camera |
|
| 26 |
+- Profile multiple bazate pe UUID (`ID_FS_UUID`) |
|
| 27 |
+- Import automat la conectarea camerei |
|
| 28 |
+- Import manual pentru un profil, un UUID sau toate profilele |
|
| 29 |
+- Organizare media pe data + conversie QuickTime UTC |
|
| 30 |
+- Curatare fisiere `.glv` (util pentru Varia/Garmin) |
|
| 31 |
+ |
|
| 32 |
+## Configurare profil |
|
| 33 |
+ |
|
| 34 |
+Fisier configurare: |
|
| 35 |
+ |
|
| 36 |
+`/etc/rpi-camera-importer/cameras.conf` |
|
| 37 |
+ |
|
| 38 |
+Format linie: |
|
| 39 |
+ |
|
| 40 |
+`name|uuid|destination_path` |
|
| 41 |
+ |
|
| 42 |
+Exemplu: |
|
| 43 |
+ |
|
| 44 |
+`varia_rct715|A1B2-C3D4|/srv/media/varia` |
|
| 45 |
+ |
|
| 46 |
+## Instalare |
|
| 47 |
+ |
|
| 48 |
+```bash |
|
| 49 |
+sudo ./setup.sh |
|
| 50 |
+``` |
|
| 51 |
+ |
|
| 52 |
+## Utilizare |
|
| 53 |
+ |
|
| 54 |
+Wizard: |
|
| 55 |
+ |
|
| 56 |
+```bash |
|
| 57 |
+rpi-camera-importer wizard |
|
| 58 |
+``` |
|
| 59 |
+ |
|
| 60 |
+Listare profile: |
|
| 61 |
+ |
|
| 62 |
+```bash |
|
| 63 |
+rpi-camera-importer list |
|
| 64 |
+``` |
|
| 65 |
+ |
|
| 66 |
+Detectie UUID-uri conectate: |
|
| 67 |
+ |
|
| 68 |
+```bash |
|
| 69 |
+rpi-camera-importer discover |
|
| 70 |
+``` |
|
| 71 |
+ |
|
| 72 |
+Import manual dupa nume profil: |
|
| 73 |
+ |
|
| 74 |
+```bash |
|
| 75 |
+rpi-camera-importer import --profile varia_rct715 |
|
| 76 |
+``` |
|
| 77 |
+ |
|
| 78 |
+Import manual dupa UUID: |
|
| 79 |
+ |
|
| 80 |
+```bash |
|
| 81 |
+rpi-camera-importer import --uuid A1B2-C3D4 |
|
| 82 |
+``` |
|
| 83 |
+ |
|
| 84 |
+Import toate profilele active: |
|
| 85 |
+ |
|
| 86 |
+```bash |
|
| 87 |
+rpi-camera-importer import --all |
|
| 88 |
+``` |
|
| 89 |
+ |
|
| 90 |
+Dry-run: |
|
| 91 |
+ |
|
| 92 |
+```bash |
|
| 93 |
+rpi-camera-importer import --all --dry-run --verbose |
|
| 94 |
+``` |
|
| 95 |
+ |
|
| 96 |
+## Fisiere runtime instalate |
|
| 97 |
+ |
|
| 98 |
+- `/usr/local/bin/rpi-camera-importer` |
|
| 99 |
+- `/usr/local/lib/rpi-camera-importer/autonas-media-importer.sh` |
|
| 100 |
+- `/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh` |
|
| 101 |
+- `/usr/local/lib/rpi-camera-importer/rpi-camera-udev-wrapper.sh` |
|
| 102 |
+- `/etc/udev/rules.d/99-rpi-camera-importer.rules` |
|
| 103 |
+- `/etc/systemd/system/rpi-camera-importer-attach@.service` |
|
@@ -0,0 +1,26 @@ |
||
| 1 |
+# Raspberry Pi Camera Importer udev rules |
|
| 2 |
+# Attach flow mirrors the proven AutoNAS trigger model (systemd attach unit + wrapper for ata bridges). |
|
| 3 |
+ |
|
| 4 |
+# USB block devices (only removable, to exclude Pi's own rootfs) |
|
| 5 |
+ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="disk", ENV{ID_BUS}=="usb", ATTRS{removable}=="1", RUN+="/bin/systemctl start --no-block rpi-camera-importer-attach@%E{ID_FS_UUID}.service"
|
|
| 6 |
+ACTION=="remove", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="disk", ENV{ID_BUS}=="usb", ATTRS{removable}=="1", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh detach %E{ID_FS_UUID}"
|
|
| 7 |
+ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="partition", ENV{ID_BUS}=="usb", ATTRS{removable}=="1", RUN+="/bin/systemctl start --no-block rpi-camera-importer-attach@%E{ID_FS_UUID}.service"
|
|
| 8 |
+ACTION=="remove", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="partition", ENV{ID_BUS}=="usb", ATTRS{removable}=="1", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh detach %E{ID_FS_UUID}"
|
|
| 9 |
+ |
|
| 10 |
+# ATA removable devices (via wrapper, as in AutoNAS) |
|
| 11 |
+ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="disk", ENV{ID_BUS}=="ata", ATTRS{removable}=="1", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-udev-wrapper.sh attach %E{ID_FS_UUID}"
|
|
| 12 |
+ACTION=="remove", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="disk", ENV{ID_BUS}=="ata", ATTRS{removable}=="1", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh detach %E{ID_FS_UUID}"
|
|
| 13 |
+ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="partition", ENV{ID_BUS}=="ata", ATTRS{removable}=="1", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-udev-wrapper.sh attach %E{ID_FS_UUID}"
|
|
| 14 |
+ACTION=="remove", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="partition", ENV{ID_BUS}=="ata", ATTRS{removable}=="1", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh detach %E{ID_FS_UUID}"
|
|
| 15 |
+ |
|
| 16 |
+# USB-SATA bridges reported as ATA + ID_USB_TYPE=disk |
|
| 17 |
+ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="disk", ENV{ID_BUS}=="ata", ENV{ID_USB_TYPE}=="disk", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-udev-wrapper.sh attach %E{ID_FS_UUID}"
|
|
| 18 |
+ACTION=="remove", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="disk", ENV{ID_BUS}=="ata", ENV{ID_USB_TYPE}=="disk", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh detach %E{ID_FS_UUID}"
|
|
| 19 |
+ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="partition", ENV{ID_BUS}=="ata", ENV{ID_USB_TYPE}=="disk", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-udev-wrapper.sh attach %E{ID_FS_UUID}"
|
|
| 20 |
+ACTION=="remove", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="partition", ENV{ID_BUS}=="ata", ENV{ID_USB_TYPE}=="disk", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh detach %E{ID_FS_UUID}"
|
|
| 21 |
+ |
|
| 22 |
+# Other removable storage |
|
| 23 |
+ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="disk", ATTRS{removable}=="1", ENV{ID_BUS}!="usb", RUN+="/bin/systemctl start --no-block rpi-camera-importer-attach@%E{ID_FS_UUID}.service"
|
|
| 24 |
+ACTION=="remove", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="disk", ATTRS{removable}=="1", ENV{ID_BUS}!="usb", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh detach %E{ID_FS_UUID}"
|
|
| 25 |
+ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="partition", ATTRS{removable}=="1", ENV{ID_BUS}!="usb", RUN+="/bin/systemctl start --no-block rpi-camera-importer-attach@%E{ID_FS_UUID}.service"
|
|
| 26 |
+ACTION=="remove", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", ENV{DEVTYPE}=="partition", ATTRS{removable}=="1", ENV{ID_BUS}!="usb", RUN+="/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh detach %E{ID_FS_UUID}"
|
|
@@ -0,0 +1,4 @@ |
||
| 1 |
+# Camera profiles for rpi-camera-importer |
|
| 2 |
+# Format: name|uuid|destination_path |
|
| 3 |
+# Example: |
|
| 4 |
+# varia_rct715|A1B2-C3D4|/srv/media/varia |
|
@@ -0,0 +1,14 @@ |
||
| 1 |
+[Unit] |
|
| 2 |
+Description=Raspberry Pi Camera Importer Attach %i |
|
| 3 |
+After=systemd-udev-settle.service |
|
| 4 |
+ |
|
| 5 |
+[Service] |
|
| 6 |
+Type=oneshot |
|
| 7 |
+ExecStart=/usr/local/lib/rpi-camera-importer/rpi-camera-disk-handler.sh attach-deferred %i |
|
| 8 |
+RemainAfterExit=no |
|
| 9 |
+TimeoutSec=120 |
|
| 10 |
+StandardOutput=journal |
|
| 11 |
+StandardError=journal |
|
| 12 |
+ |
|
| 13 |
+[Install] |
|
| 14 |
+WantedBy=multi-user.target |
|
@@ -0,0 +1,438 @@ |
||
| 1 |
+#!/bin/bash |
|
| 2 |
+ |
|
| 3 |
+# AutoNAS Media Importer |
|
| 4 |
+# Advanced media import engine that processes, organizes and imports media files from cameras |
|
| 5 |
+# Usage: autonas-media-importer.sh <source_mount> <destination_path> |
|
| 6 |
+ |
|
| 7 |
+# Global configuration |
|
| 8 |
+LOG_TAG="autonas-import" |
|
| 9 |
+ |
|
| 10 |
+# Function to log messages |
|
| 11 |
+log_message() {
|
|
| 12 |
+ local message="$1" |
|
| 13 |
+ local priority="${2:-info}" # Default priority is info
|
|
| 14 |
+ |
|
| 15 |
+ # Log to syslog with facility local0 and specified priority |
|
| 16 |
+ logger -p "local0.$priority" -t "$LOG_TAG" "$message" |
|
| 17 |
+ |
|
| 18 |
+ # Also echo to stdout/stderr for interactive use |
|
| 19 |
+ if [ -t 1 ]; then |
|
| 20 |
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - $message" |
|
| 21 |
+ fi |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+# Usage function |
|
| 25 |
+usage() {
|
|
| 26 |
+ echo "Usage: $0 <source_mount> <destination_path> [options]" |
|
| 27 |
+ echo "" |
|
| 28 |
+ echo "Arguments:" |
|
| 29 |
+ echo " source_mount - Mount point of the camera (e.g., /mnt/autonas/camera)" |
|
| 30 |
+ echo " destination_path - Destination directory for imported files" |
|
| 31 |
+ echo "" |
|
| 32 |
+ echo "Options:" |
|
| 33 |
+ echo " --dry-run - Show what would be done without actually doing it" |
|
| 34 |
+ echo " --keep-originals - Keep original files on camera after import" |
|
| 35 |
+ echo " --verbose - Enable verbose output" |
|
| 36 |
+ echo " --limit N - Process only N files (useful for testing)" |
|
| 37 |
+ echo " --help - Show this help" |
|
| 38 |
+ echo "" |
|
| 39 |
+ echo "Examples:" |
|
| 40 |
+ echo " $0 /mnt/autonas/camera /mnt/autonas/photos/imported" |
|
| 41 |
+ echo " $0 /mnt/autonas/camera /mnt/autonas/photos/imported --dry-run --verbose" |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+# Parse command line arguments |
|
| 45 |
+SOURCE_MOUNT="" |
|
| 46 |
+DESTINATION="" |
|
| 47 |
+DRY_RUN=0 |
|
| 48 |
+KEEP_ORIGINALS=0 |
|
| 49 |
+VERBOSE=0 |
|
| 50 |
+FILE_LIMIT=0 |
|
| 51 |
+ |
|
| 52 |
+while [[ $# -gt 0 ]]; do |
|
| 53 |
+ case $1 in |
|
| 54 |
+ --dry-run) |
|
| 55 |
+ DRY_RUN=1 |
|
| 56 |
+ shift |
|
| 57 |
+ ;; |
|
| 58 |
+ --keep-originals) |
|
| 59 |
+ KEEP_ORIGINALS=1 |
|
| 60 |
+ shift |
|
| 61 |
+ ;; |
|
| 62 |
+ --verbose) |
|
| 63 |
+ VERBOSE=1 |
|
| 64 |
+ shift |
|
| 65 |
+ ;; |
|
| 66 |
+ --limit) |
|
| 67 |
+ FILE_LIMIT="$2" |
|
| 68 |
+ if ! [[ "$FILE_LIMIT" =~ ^[0-9]+$ ]]; then |
|
| 69 |
+ echo "Error: --limit requires a number" |
|
| 70 |
+ usage |
|
| 71 |
+ exit 1 |
|
| 72 |
+ fi |
|
| 73 |
+ shift 2 |
|
| 74 |
+ ;; |
|
| 75 |
+ --help) |
|
| 76 |
+ usage |
|
| 77 |
+ exit 0 |
|
| 78 |
+ ;; |
|
| 79 |
+ -*) |
|
| 80 |
+ echo "Unknown option: $1" |
|
| 81 |
+ usage |
|
| 82 |
+ exit 1 |
|
| 83 |
+ ;; |
|
| 84 |
+ *) |
|
| 85 |
+ if [[ -z "$SOURCE_MOUNT" ]]; then |
|
| 86 |
+ SOURCE_MOUNT="$1" |
|
| 87 |
+ elif [[ -z "$DESTINATION" ]]; then |
|
| 88 |
+ DESTINATION="$1" |
|
| 89 |
+ else |
|
| 90 |
+ echo "Too many arguments" |
|
| 91 |
+ usage |
|
| 92 |
+ exit 1 |
|
| 93 |
+ fi |
|
| 94 |
+ shift |
|
| 95 |
+ ;; |
|
| 96 |
+ esac |
|
| 97 |
+done |
|
| 98 |
+ |
|
| 99 |
+# Validate arguments |
|
| 100 |
+if [[ -z "$SOURCE_MOUNT" || -z "$DESTINATION" ]]; then |
|
| 101 |
+ echo "Error: Both source_mount and destination_path are required" |
|
| 102 |
+ usage |
|
| 103 |
+ exit 1 |
|
| 104 |
+fi |
|
| 105 |
+ |
|
| 106 |
+# Check if source exists and is mounted |
|
| 107 |
+if [[ ! -d "$SOURCE_MOUNT" ]]; then |
|
| 108 |
+ log_message "Error: Source mount point does not exist: $SOURCE_MOUNT" "err" |
|
| 109 |
+ exit 1 |
|
| 110 |
+fi |
|
| 111 |
+ |
|
| 112 |
+# Check if source is actually mounted |
|
| 113 |
+if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then |
|
| 114 |
+ log_message "Warning: Source path is not a mount point: $SOURCE_MOUNT" "warning" |
|
| 115 |
+fi |
|
| 116 |
+ |
|
| 117 |
+# Create destination directory if it doesn't exist |
|
| 118 |
+if [[ $DRY_RUN -eq 0 ]]; then |
|
| 119 |
+ mkdir -p "$DESTINATION" |
|
| 120 |
+ if [[ ! -d "$DESTINATION" ]]; then |
|
| 121 |
+ log_message "Error: Cannot create destination directory: $DESTINATION" "err" |
|
| 122 |
+ exit 1 |
|
| 123 |
+ fi |
|
| 124 |
+fi |
|
| 125 |
+ |
|
| 126 |
+# Check for required tools |
|
| 127 |
+if ! command -v exiftool &> /dev/null; then |
|
| 128 |
+ log_message "Error: exiftool is required but not installed" "err" |
|
| 129 |
+ exit 1 |
|
| 130 |
+fi |
|
| 131 |
+ |
|
| 132 |
+# Function to process a single file |
|
| 133 |
+process_file() {
|
|
| 134 |
+ local file="$1" |
|
| 135 |
+ local relative_path="${file#$SOURCE_MOUNT/}"
|
|
| 136 |
+ |
|
| 137 |
+ # Check if source mount is still available |
|
| 138 |
+ if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then |
|
| 139 |
+ log_message "Error: Source mount point is no longer available: $SOURCE_MOUNT" "err" |
|
| 140 |
+ return 1 |
|
| 141 |
+ fi |
|
| 142 |
+ |
|
| 143 |
+ # Check if file still exists |
|
| 144 |
+ if [[ ! -f "$file" ]]; then |
|
| 145 |
+ log_message "Error: File no longer exists: $relative_path" "err" |
|
| 146 |
+ log_message "Camera appears to be disconnected, stopping import" "warning" |
|
| 147 |
+ exit 1 |
|
| 148 |
+ fi |
|
| 149 |
+ |
|
| 150 |
+ if [[ $VERBOSE -eq 1 ]]; then |
|
| 151 |
+ log_message "Processing: $relative_path" "info" |
|
| 152 |
+ fi |
|
| 153 |
+ |
|
| 154 |
+ # Check which group CreateDate comes from to determine correct handling |
|
| 155 |
+ create_date_info=$(exiftool -G1 -s -CreateDate "$file" 2>/dev/null | grep CreateDate | head -1) |
|
| 156 |
+ |
|
| 157 |
+ # Check if exiftool failed (possible if device disconnected) |
|
| 158 |
+ local exiftool_exit_code=$? |
|
| 159 |
+ if [[ $exiftool_exit_code -ne 0 ]] && [[ $exiftool_exit_code -ne 1 ]]; then |
|
| 160 |
+ log_message "Error: Cannot read file (device may be disconnected): $relative_path" "err" |
|
| 161 |
+ log_message "Camera appears to be disconnected, stopping import" "warning" |
|
| 162 |
+ exit 1 |
|
| 163 |
+ fi |
|
| 164 |
+ |
|
| 165 |
+ if [[ -z "$create_date_info" ]]; then |
|
| 166 |
+ log_message "Warning: No CreateDate found in $relative_path, using file modification time" "warning" |
|
| 167 |
+ # Fallback to file modification time |
|
| 168 |
+ local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S" 2>/dev/null) |
|
| 169 |
+ if [[ -n "$file_date" ]]; then |
|
| 170 |
+ create_date_value="$file_date" |
|
| 171 |
+ create_date_group="FileSystem" |
|
| 172 |
+ else |
|
| 173 |
+ log_message "Error: Cannot determine date for $relative_path" "err" |
|
| 174 |
+ return 1 |
|
| 175 |
+ fi |
|
| 176 |
+ else |
|
| 177 |
+ create_date_group=$(echo "$create_date_info" | cut -d']' -f1 | cut -d'[' -f2) |
|
| 178 |
+ create_date_value=$(echo "$create_date_info" | cut -d':' -f2- | xargs) |
|
| 179 |
+ # Convert EXIF date format (YYYY:MM:DD HH:MM:SS) to standard format (YYYY-MM-DD HH:MM:SS) |
|
| 180 |
+ create_date_value=$(echo "$create_date_value" | sed 's/^\([0-9]\{4\}\):\([0-9]\{2\}\):\([0-9]\{2\}\)/\1-\2-\3/')
|
|
| 181 |
+ fi |
|
| 182 |
+ |
|
| 183 |
+ if [[ $VERBOSE -eq 1 ]]; then |
|
| 184 |
+ echo -n " Date: [$create_date_value] from $create_date_group " |
|
| 185 |
+ fi |
|
| 186 |
+ |
|
| 187 |
+ # Extract file extension |
|
| 188 |
+ local filename=$(basename "$file") |
|
| 189 |
+ local extension="${filename##*.}"
|
|
| 190 |
+ |
|
| 191 |
+ # For QuickTime files, the CreateDate is in UTC and needs conversion to local time |
|
| 192 |
+ if [[ "$create_date_group" == "QuickTime" ]]; then |
|
| 193 |
+ # Convert UTC time to local time |
|
| 194 |
+ local utc_timestamp=$(date -d "$create_date_value UTC" "+%s" 2>/dev/null) |
|
| 195 |
+ if [[ -n "$utc_timestamp" ]]; then |
|
| 196 |
+ create_date_value=$(date -d "@$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null) |
|
| 197 |
+ if [[ $VERBOSE -eq 1 ]]; then |
|
| 198 |
+ echo -n "(converted from UTC) " |
|
| 199 |
+ fi |
|
| 200 |
+ fi |
|
| 201 |
+ fi |
|
| 202 |
+ |
|
| 203 |
+ # Create output directory structure |
|
| 204 |
+ local date_dir=$(date -d "$create_date_value" "+%Y-%m-%d" 2>/dev/null) |
|
| 205 |
+ if [[ -z "$date_dir" ]]; then |
|
| 206 |
+ log_message "Error: Invalid date format for $relative_path: $create_date_value" "err" |
|
| 207 |
+ return 1 |
|
| 208 |
+ fi |
|
| 209 |
+ |
|
| 210 |
+ local output_dir="$DESTINATION/$date_dir" |
|
| 211 |
+ |
|
| 212 |
+ if [[ $DRY_RUN -eq 0 ]]; then |
|
| 213 |
+ mkdir -p "$output_dir" |
|
| 214 |
+ fi |
|
| 215 |
+ |
|
| 216 |
+ # Generate output filename with timestamp |
|
| 217 |
+ local timestamp=$(date -d "$create_date_value" "+%Y-%m-%d_%H-%M-%S" 2>/dev/null) |
|
| 218 |
+ local output_filename="${timestamp}.${extension,,}" # Convert extension to lowercase
|
|
| 219 |
+ local output_path="$output_dir/$output_filename" |
|
| 220 |
+ |
|
| 221 |
+ # Handle filename conflicts |
|
| 222 |
+ local counter=1 |
|
| 223 |
+ local base_output_path="$output_path" |
|
| 224 |
+ while [[ -f "$output_path" ]] && [[ $DRY_RUN -eq 0 ]]; do |
|
| 225 |
+ local name_without_ext="${timestamp}_${counter}"
|
|
| 226 |
+ output_path="$output_dir/${name_without_ext}.${extension,,}"
|
|
| 227 |
+ counter=$((counter + 1)) |
|
| 228 |
+ done |
|
| 229 |
+ |
|
| 230 |
+ if [[ $DRY_RUN -eq 1 ]]; then |
|
| 231 |
+ if [[ $KEEP_ORIGINALS -eq 1 ]]; then |
|
| 232 |
+ echo "Would copy: $relative_path -> ${output_path#$DESTINATION/}"
|
|
| 233 |
+ else |
|
| 234 |
+ echo "Would move: $relative_path -> ${output_path#$DESTINATION/}"
|
|
| 235 |
+ fi |
|
| 236 |
+ else |
|
| 237 |
+ # Perform the actual file operation |
|
| 238 |
+ if [[ $KEEP_ORIGINALS -eq 1 ]]; then |
|
| 239 |
+ if cp "$file" "$output_path"; then |
|
| 240 |
+ if [[ $VERBOSE -eq 1 ]]; then |
|
| 241 |
+ echo "✓ Copied" |
|
| 242 |
+ fi |
|
| 243 |
+ log_message "Copied: $relative_path -> ${output_path#$DESTINATION/}" "info"
|
|
| 244 |
+ return 0 |
|
| 245 |
+ else |
|
| 246 |
+ if [[ $VERBOSE -eq 1 ]]; then |
|
| 247 |
+ echo "✗ Copy failed" |
|
| 248 |
+ fi |
|
| 249 |
+ log_message "Error: Failed to copy $relative_path" "err" |
|
| 250 |
+ return 1 |
|
| 251 |
+ fi |
|
| 252 |
+ else |
|
| 253 |
+ if mv "$file" "$output_path"; then |
|
| 254 |
+ if [[ $VERBOSE -eq 1 ]]; then |
|
| 255 |
+ echo "✓ Moved" |
|
| 256 |
+ fi |
|
| 257 |
+ log_message "Moved: $relative_path -> ${output_path#$DESTINATION/}" "info"
|
|
| 258 |
+ return 0 |
|
| 259 |
+ else |
|
| 260 |
+ if [[ $VERBOSE -eq 1 ]]; then |
|
| 261 |
+ echo "✗ Move failed" |
|
| 262 |
+ fi |
|
| 263 |
+ log_message "Error: Failed to move $relative_path" "err" |
|
| 264 |
+ return 1 |
|
| 265 |
+ fi |
|
| 266 |
+ fi |
|
| 267 |
+ fi |
|
| 268 |
+} |
|
| 269 |
+ |
|
| 270 |
+# Function to find camera directories |
|
| 271 |
+find_camera_directories() {
|
|
| 272 |
+ local search_patterns=("DCIM" "PRIVATE" "MP_ROOT" "AVCHD" "Photos" "Videos")
|
|
| 273 |
+ local found_dirs=() |
|
| 274 |
+ |
|
| 275 |
+ # Test if the mount point is accessible with a timeout |
|
| 276 |
+ if ! timeout 3 ls "$SOURCE_MOUNT" >/dev/null 2>&1; then |
|
| 277 |
+ log_message "Error: Mount point is not accessible (device likely disconnected): $SOURCE_MOUNT" "err" |
|
| 278 |
+ exit 1 |
|
| 279 |
+ fi |
|
| 280 |
+ |
|
| 281 |
+ for pattern in "${search_patterns[@]}"; do
|
|
| 282 |
+ while IFS= read -r -d '' dir; do |
|
| 283 |
+ found_dirs+=("$dir")
|
|
| 284 |
+ done < <(timeout 5 find "$SOURCE_MOUNT" -maxdepth 3 -type d -iname "$pattern" -print0 2>/dev/null) |
|
| 285 |
+ done |
|
| 286 |
+ |
|
| 287 |
+ # If no camera directories found, search for common media file extensions |
|
| 288 |
+ if [[ ${#found_dirs[@]} -eq 0 ]]; then
|
|
| 289 |
+ log_message "No camera directories found, searching for media files..." "info" |
|
| 290 |
+ local media_extensions=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.cr2" "*.nef" "*.arw" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts")
|
|
| 291 |
+ |
|
| 292 |
+ for ext in "${media_extensions[@]}"; do
|
|
| 293 |
+ while IFS= read -r -d '' file; do |
|
| 294 |
+ local dir=$(dirname "$file") |
|
| 295 |
+ if [[ ! " ${found_dirs[@]} " =~ " ${dir} " ]]; then
|
|
| 296 |
+ found_dirs+=("$dir")
|
|
| 297 |
+ fi |
|
| 298 |
+ done < <(timeout 5 find "$SOURCE_MOUNT" -maxdepth 3 -type f -iname "$ext" -print0 2>/dev/null) |
|
| 299 |
+ done |
|
| 300 |
+ fi |
|
| 301 |
+ |
|
| 302 |
+ printf '%s\n' "${found_dirs[@]}" | sort -u
|
|
| 303 |
+} |
|
| 304 |
+ |
|
| 305 |
+# Main execution |
|
| 306 |
+log_message "Starting camera import from $SOURCE_MOUNT to $DESTINATION" "info" |
|
| 307 |
+ |
|
| 308 |
+if [[ $DRY_RUN -eq 1 ]]; then |
|
| 309 |
+ log_message "DRY RUN MODE - No files will be actually moved/copied" "info" |
|
| 310 |
+fi |
|
| 311 |
+ |
|
| 312 |
+if [[ $KEEP_ORIGINALS -eq 1 ]]; then |
|
| 313 |
+ log_message "KEEP ORIGINALS MODE - Files will be copied instead of moved" "info" |
|
| 314 |
+fi |
|
| 315 |
+ |
|
| 316 |
+# Find camera directories |
|
| 317 |
+log_message "Scanning for camera directories..." "info" |
|
| 318 |
+camera_dirs=$(find_camera_directories) |
|
| 319 |
+ |
|
| 320 |
+if [[ -z "$camera_dirs" ]]; then |
|
| 321 |
+ log_message "No camera directories or media files found in $SOURCE_MOUNT" "warning" |
|
| 322 |
+ exit 0 |
|
| 323 |
+fi |
|
| 324 |
+ |
|
| 325 |
+echo "Found camera directories:" |
|
| 326 |
+echo "$camera_dirs" | while IFS= read -r dir; do |
|
| 327 |
+ echo " $dir" |
|
| 328 |
+done |
|
| 329 |
+ |
|
| 330 |
+# Process files |
|
| 331 |
+total_files=0 |
|
| 332 |
+processed_files=0 |
|
| 333 |
+error_files=0 |
|
| 334 |
+ |
|
| 335 |
+# Delete GLV files (Garmin video preview files) - they're usually not needed |
|
| 336 |
+log_message "Cleaning up GLV preview files..." "info" |
|
| 337 |
+glv_count=0 |
|
| 338 |
+while IFS= read -r dir; do |
|
| 339 |
+ if [[ $DRY_RUN -eq 1 ]]; then |
|
| 340 |
+ glv_files=$(find "$dir" -type f -iname "*.glv" 2>/dev/null | wc -l) |
|
| 341 |
+ if [[ $glv_files -gt 0 ]]; then |
|
| 342 |
+ echo "Would delete $glv_files GLV files from $dir" |
|
| 343 |
+ glv_count=$((glv_count + glv_files)) |
|
| 344 |
+ fi |
|
| 345 |
+ else |
|
| 346 |
+ while IFS= read -r -d '' glv_file; do |
|
| 347 |
+ if rm "$glv_file" 2>/dev/null; then |
|
| 348 |
+ glv_count=$((glv_count + 1)) |
|
| 349 |
+ fi |
|
| 350 |
+ done < <(find "$dir" -type f -iname "*.glv" -print0 2>/dev/null) |
|
| 351 |
+ fi |
|
| 352 |
+done <<< "$camera_dirs" |
|
| 353 |
+ |
|
| 354 |
+if [[ $glv_count -gt 0 ]]; then |
|
| 355 |
+ if [[ $DRY_RUN -eq 1 ]]; then |
|
| 356 |
+ log_message "Would delete $glv_count GLV preview files" "info" |
|
| 357 |
+ else |
|
| 358 |
+ log_message "Deleted $glv_count GLV preview files" "info" |
|
| 359 |
+ fi |
|
| 360 |
+fi |
|
| 361 |
+ |
|
| 362 |
+# Process media files |
|
| 363 |
+log_message "Processing media files..." "info" |
|
| 364 |
+media_extensions=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.tif" "*.cr2" "*.nef" "*.arw" "*.dng" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts" "*.mkv" "*.wmv")
|
|
| 365 |
+ |
|
| 366 |
+# In dry-run mode, limit output to avoid overwhelming logs |
|
| 367 |
+max_files_to_show=20 |
|
| 368 |
+files_shown=0 |
|
| 369 |
+files_processed_count=0 |
|
| 370 |
+ |
|
| 371 |
+while IFS= read -r dir; do |
|
| 372 |
+ # Check if mount point is still available before processing each directory |
|
| 373 |
+ if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then |
|
| 374 |
+ log_message "Camera disconnected, stopping import process" "warning" |
|
| 375 |
+ break |
|
| 376 |
+ fi |
|
| 377 |
+ |
|
| 378 |
+ for ext in "${media_extensions[@]}"; do
|
|
| 379 |
+ while IFS= read -r -d '' file; do |
|
| 380 |
+ total_files=$((total_files + 1)) |
|
| 381 |
+ files_processed_count=$((files_processed_count + 1)) |
|
| 382 |
+ |
|
| 383 |
+ # Check if camera is still connected before processing each file |
|
| 384 |
+ if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then |
|
| 385 |
+ log_message "Camera disconnected during processing, stopping import" "warning" |
|
| 386 |
+ exit 1 |
|
| 387 |
+ fi |
|
| 388 |
+ |
|
| 389 |
+ # Check file limit |
|
| 390 |
+ if [[ $FILE_LIMIT -gt 0 && $files_processed_count -gt $FILE_LIMIT ]]; then |
|
| 391 |
+ echo "Reached file limit of $FILE_LIMIT files, stopping processing..." |
|
| 392 |
+ break 3 |
|
| 393 |
+ fi |
|
| 394 |
+ |
|
| 395 |
+ # In dry-run mode, limit verbose output |
|
| 396 |
+ if [[ $DRY_RUN -eq 1 && $files_shown -ge $max_files_to_show ]]; then |
|
| 397 |
+ if [[ $files_shown -eq $max_files_to_show ]]; then |
|
| 398 |
+ echo "... (limiting output in dry-run mode, processing continues)" |
|
| 399 |
+ files_shown=$((files_shown + 1)) |
|
| 400 |
+ fi |
|
| 401 |
+ # Still process but don't show details |
|
| 402 |
+ processed_files=$((processed_files + 1)) |
|
| 403 |
+ else |
|
| 404 |
+ if [[ $DRY_RUN -eq 1 ]]; then |
|
| 405 |
+ files_shown=$((files_shown + 1)) |
|
| 406 |
+ fi |
|
| 407 |
+ |
|
| 408 |
+ if process_file "$file"; then |
|
| 409 |
+ processed_files=$((processed_files + 1)) |
|
| 410 |
+ else |
|
| 411 |
+ error_files=$((error_files + 1)) |
|
| 412 |
+ fi |
|
| 413 |
+ fi |
|
| 414 |
+ done < <(find "$dir" -type f -iname "$ext" -print0 2>/dev/null) |
|
| 415 |
+ done |
|
| 416 |
+done <<< "$camera_dirs" |
|
| 417 |
+ |
|
| 418 |
+# Summary |
|
| 419 |
+log_message "Import completed: $processed_files/$total_files files processed successfully" "info" |
|
| 420 |
+if [[ $error_files -gt 0 ]]; then |
|
| 421 |
+ log_message "Import had errors: $error_files files failed to process" "warning" |
|
| 422 |
+fi |
|
| 423 |
+ |
|
| 424 |
+echo "" |
|
| 425 |
+echo "=== Import Summary ===" |
|
| 426 |
+echo "Total files found: $total_files" |
|
| 427 |
+echo "Successfully processed: $processed_files" |
|
| 428 |
+echo "Errors: $error_files" |
|
| 429 |
+if [[ $glv_count -gt 0 ]]; then |
|
| 430 |
+ echo "GLV files cleaned up: $glv_count" |
|
| 431 |
+fi |
|
| 432 |
+ |
|
| 433 |
+# Exit with error code if there were errors |
|
| 434 |
+if [[ $error_files -gt 0 ]]; then |
|
| 435 |
+ exit 1 |
|
| 436 |
+else |
|
| 437 |
+ exit 0 |
|
| 438 |
+fi |
|
@@ -0,0 +1,59 @@ |
||
| 1 |
+#!/usr/bin/env bash |
|
| 2 |
+ |
|
| 3 |
+set -euo pipefail |
|
| 4 |
+ |
|
| 5 |
+LOG_TAG="rpi-camera-disk-handler" |
|
| 6 |
+APP_BIN="/usr/local/bin/rpi-camera-importer" |
|
| 7 |
+ |
|
| 8 |
+log_message() {
|
|
| 9 |
+ local message="$1" |
|
| 10 |
+ local priority="${2:-info}"
|
|
| 11 |
+ logger -p "local0.${priority}" -t "$LOG_TAG" "$message"
|
|
| 12 |
+} |
|
| 13 |
+ |
|
| 14 |
+usage() {
|
|
| 15 |
+ echo "Usage: $0 {attach-deferred|detach} <UUID>"
|
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+action="${1:-}"
|
|
| 19 |
+uuid="${2:-}"
|
|
| 20 |
+attach_dry_run="${RPI_CAMERA_IMPORTER_ATTACH_DRY_RUN:-0}"
|
|
| 21 |
+ |
|
| 22 |
+if [[ -z "$action" || -z "$uuid" ]]; then |
|
| 23 |
+ log_message "Invalid arguments: action='$action' uuid='$uuid'" "err" |
|
| 24 |
+ usage |
|
| 25 |
+ exit 1 |
|
| 26 |
+fi |
|
| 27 |
+ |
|
| 28 |
+case "$action" in |
|
| 29 |
+ attach-deferred) |
|
| 30 |
+ log_message "Attach deferred triggered for UUID=$uuid" |
|
| 31 |
+ if [[ ! -x "$APP_BIN" ]]; then |
|
| 32 |
+ log_message "CLI not found: $APP_BIN" "err" |
|
| 33 |
+ exit 1 |
|
| 34 |
+ fi |
|
| 35 |
+ |
|
| 36 |
+ # Run import only for configured UUID; unconfigured devices exit cleanly. |
|
| 37 |
+ import_args=(import --uuid "$uuid") |
|
| 38 |
+ if [[ "$attach_dry_run" == "1" ]]; then |
|
| 39 |
+ import_args+=(--dry-run --verbose) |
|
| 40 |
+ log_message "Attach handler running in dry-run mode (RPI_CAMERA_IMPORTER_ATTACH_DRY_RUN=1)" |
|
| 41 |
+ fi |
|
| 42 |
+ |
|
| 43 |
+ if ! "$APP_BIN" "${import_args[@]}"; then
|
|
| 44 |
+ log_message "Import flow failed for UUID=$uuid" "err" |
|
| 45 |
+ exit 1 |
|
| 46 |
+ fi |
|
| 47 |
+ ;; |
|
| 48 |
+ detach) |
|
| 49 |
+ # No detach-side action required; importer handles mount/unmount around imports. |
|
| 50 |
+ log_message "Detach event for UUID=$uuid (no-op)" |
|
| 51 |
+ ;; |
|
| 52 |
+ *) |
|
| 53 |
+ log_message "Unsupported action: $action" "err" |
|
| 54 |
+ usage |
|
| 55 |
+ exit 1 |
|
| 56 |
+ ;; |
|
| 57 |
+esac |
|
| 58 |
+ |
|
| 59 |
+exit 0 |
|
@@ -0,0 +1,676 @@ |
||
| 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 "$@" |
|
@@ -0,0 +1,39 @@ |
||
| 1 |
+#!/usr/bin/env bash |
|
| 2 |
+ |
|
| 3 |
+set -euo pipefail |
|
| 4 |
+ |
|
| 5 |
+LOG_TAG="rpi-camera-wrapper" |
|
| 6 |
+export PATH="/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/sbin:/usr/sbin" |
|
| 7 |
+export HOME="/root" |
|
| 8 |
+export USER="root" |
|
| 9 |
+export LOGNAME="root" |
|
| 10 |
+export SHELL="/bin/bash" |
|
| 11 |
+umask 022 |
|
| 12 |
+ |
|
| 13 |
+action="${1:-}"
|
|
| 14 |
+uuid="${2:-}"
|
|
| 15 |
+ |
|
| 16 |
+if [[ -z "$action" || -z "$uuid" ]]; then |
|
| 17 |
+ logger -p local0.err -t "$LOG_TAG" "Invalid arguments: action='$action' uuid='$uuid'" |
|
| 18 |
+ exit 1 |
|
| 19 |
+fi |
|
| 20 |
+ |
|
| 21 |
+logger -p local0.info -t "$LOG_TAG" "Called with action=$action uuid=$uuid" |
|
| 22 |
+ |
|
| 23 |
+( |
|
| 24 |
+ sleep 3 |
|
| 25 |
+ if [[ "$(id -u)" != "0" ]]; then |
|
| 26 |
+ logger -p local0.err -t "$LOG_TAG" "Not running as root" |
|
| 27 |
+ exit 1 |
|
| 28 |
+ fi |
|
| 29 |
+ |
|
| 30 |
+ if [[ "$action" == "attach" ]]; then |
|
| 31 |
+ systemctl start "rpi-camera-importer-attach@${uuid}.service"
|
|
| 32 |
+ logger -p local0.info -t "$LOG_TAG" "Started attach service for UUID=$uuid" |
|
| 33 |
+ else |
|
| 34 |
+ logger -p local0.warning -t "$LOG_TAG" "Unsupported wrapper action: $action" |
|
| 35 |
+ fi |
|
| 36 |
+) & |
|
| 37 |
+ |
|
| 38 |
+logger -p local0.info -t "$LOG_TAG" "Background job started for $action $uuid" |
|
| 39 |
+exit 0 |
|
@@ -0,0 +1,63 @@ |
||
| 1 |
+#!/usr/bin/env bash |
|
| 2 |
+ |
|
| 3 |
+set -euo pipefail |
|
| 4 |
+ |
|
| 5 |
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
| 6 |
+APP_NAME="rpi-camera-importer" |
|
| 7 |
+BIN_PATH="/usr/local/bin/${APP_NAME}"
|
|
| 8 |
+RUNTIME_DIR="/usr/local/lib/${APP_NAME}"
|
|
| 9 |
+CONFIG_DIR="/etc/rpi-camera-importer" |
|
| 10 |
+CONFIG_FILE="${CONFIG_DIR}/cameras.conf"
|
|
| 11 |
+UDEV_RULE="/etc/udev/rules.d/99-rpi-camera-importer.rules" |
|
| 12 |
+ATTACH_UNIT="/etc/systemd/system/rpi-camera-importer-attach@.service" |
|
| 13 |
+ |
|
| 14 |
+require_root() {
|
|
| 15 |
+ if [[ "${EUID}" -ne 0 ]]; then
|
|
| 16 |
+ echo "This script must run as root" |
|
| 17 |
+ exit 1 |
|
| 18 |
+ fi |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+install_dependencies() {
|
|
| 22 |
+ echo "Dependency installation at deploy is disabled." |
|
| 23 |
+ echo "Dependencies are checked at runtime by rpi-camera-importer." |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+install_files() {
|
|
| 27 |
+ install -d /usr/local/bin |
|
| 28 |
+ install -d "$RUNTIME_DIR" |
|
| 29 |
+ install -d "$CONFIG_DIR" |
|
| 30 |
+ |
|
| 31 |
+ install -m 0755 "${SCRIPT_DIR}/scripts/rpi-camera-importer.sh" "$BIN_PATH"
|
|
| 32 |
+ install -m 0755 "${SCRIPT_DIR}/scripts/rpi-camera-disk-handler.sh" "${RUNTIME_DIR}/rpi-camera-disk-handler.sh"
|
|
| 33 |
+ install -m 0755 "${SCRIPT_DIR}/scripts/rpi-camera-udev-wrapper.sh" "${RUNTIME_DIR}/rpi-camera-udev-wrapper.sh"
|
|
| 34 |
+ install -m 0755 "${SCRIPT_DIR}/scripts/autonas-media-importer.sh" "${RUNTIME_DIR}/autonas-media-importer.sh"
|
|
| 35 |
+ |
|
| 36 |
+ if [[ ! -f "$CONFIG_FILE" ]]; then |
|
| 37 |
+ install -m 0644 "${SCRIPT_DIR}/config/cameras.conf" "$CONFIG_FILE"
|
|
| 38 |
+ else |
|
| 39 |
+ echo "Config already exists, preserving: $CONFIG_FILE" |
|
| 40 |
+ fi |
|
| 41 |
+ |
|
| 42 |
+ install -m 0644 "${SCRIPT_DIR}/config/99-rpi-camera-importer.rules" "$UDEV_RULE"
|
|
| 43 |
+ install -m 0644 "${SCRIPT_DIR}/config/rpi-camera-importer-attach@.service" "$ATTACH_UNIT"
|
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+activate() {
|
|
| 47 |
+ systemctl daemon-reload |
|
| 48 |
+ udevadm control --reload-rules |
|
| 49 |
+ udevadm trigger --subsystem-match=block --action=add |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+main() {
|
|
| 53 |
+ require_root |
|
| 54 |
+ install_dependencies |
|
| 55 |
+ install_files |
|
| 56 |
+ activate |
|
| 57 |
+ |
|
| 58 |
+ echo "Installed ${APP_NAME}"
|
|
| 59 |
+ echo "Wizard: ${APP_NAME} wizard"
|
|
| 60 |
+ echo "Logs: journalctl -u rpi-camera-importer-attach@<UUID>.service -f" |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+main "$@" |
|
@@ -16,6 +16,7 @@ COLLECT_UNSORTABLE=0 |
||
| 16 | 16 |
SOURCE_PATTERNS=() |
| 17 | 17 |
DESTINATION="" |
| 18 | 18 |
KEEP_ORIGINALS=0 |
| 19 |
+VERIFY_MODE="size" # options: size, strict, none |
|
| 19 | 20 |
DRY_RUN=0 |
| 20 | 21 |
VERBOSE=0 |
| 21 | 22 |
CLEANUP_EMPTY_DIRS=1 |
@@ -96,6 +97,7 @@ Options: |
||
| 96 | 97 |
-s, --source PATH File or directory to process (repeatable). Default: cwd |
| 97 | 98 |
-d, --destination PATH Destination folder. Required when multiple -s are given. |
| 98 | 99 |
-k, --keep-originals Copy files instead of moving |
| 100 |
+ --verify-mode MODE size|strict|none (default: size) |
|
| 99 | 101 |
--collect-unsortable Put files without dates into DEST/unsortable |
| 100 | 102 |
--keep-empty-dirs Keep empty directories after processing |
| 101 | 103 |
--dry-run Show actions without changing files |
@@ -240,6 +242,91 @@ safe_cp() {
|
||
| 240 | 242 |
return $? |
| 241 | 243 |
} |
| 242 | 244 |
|
| 245 |
+verify_copied_file() {
|
|
| 246 |
+ local src="$1" |
|
| 247 |
+ local dst="$2" |
|
| 248 |
+ local expected_date="$3" |
|
| 249 |
+ |
|
| 250 |
+ if [[ ! -f "$dst" ]]; then |
|
| 251 |
+ log_message "Verified copy missing at destination: $dst" "ERROR" |
|
| 252 |
+ return 1 |
|
| 253 |
+ fi |
|
| 254 |
+ |
|
| 255 |
+ local src_size dst_size |
|
| 256 |
+ src_size=$(get_file_size "$src") |
|
| 257 |
+ dst_size=$(get_file_size "$dst") |
|
| 258 |
+ if [[ "$src_size" != "$dst_size" ]]; then |
|
| 259 |
+ log_message "Size mismatch after copy: $src ($src_size) != $dst ($dst_size)" "ERROR" |
|
| 260 |
+ return 1 |
|
| 261 |
+ fi |
|
| 262 |
+ |
|
| 263 |
+ if [[ "$VERIFY_MODE" == "strict" ]]; then |
|
| 264 |
+ if ! cmp -s "$src" "$dst"; then |
|
| 265 |
+ log_message "Content mismatch after copy: $src -> $dst" "ERROR" |
|
| 266 |
+ return 1 |
|
| 267 |
+ fi |
|
| 268 |
+ elif [[ "$VERIFY_MODE" == "none" ]]; then |
|
| 269 |
+ return 0 |
|
| 270 |
+ fi |
|
| 271 |
+ |
|
| 272 |
+ if [[ -n "$expected_date" ]]; then |
|
| 273 |
+ local destination_date_info |
|
| 274 |
+ destination_date_info=$(extract_file_date "$dst") |
|
| 275 |
+ local extract_status=$? |
|
| 276 |
+ if [[ $extract_status -ne 0 || -z "$destination_date_info" ]]; then |
|
| 277 |
+ log_message "Destination metadata validation failed: $dst" "ERROR" |
|
| 278 |
+ return 1 |
|
| 279 |
+ fi |
|
| 280 |
+ |
|
| 281 |
+ local destination_date="${destination_date_info%|*}"
|
|
| 282 |
+ if [[ "$destination_date" != "$expected_date" ]]; then |
|
| 283 |
+ log_message "Destination metadata mismatch: expected $expected_date, got $destination_date for $dst" "ERROR" |
|
| 284 |
+ return 1 |
|
| 285 |
+ fi |
|
| 286 |
+ fi |
|
| 287 |
+ |
|
| 288 |
+ return 0 |
|
| 289 |
+} |
|
| 290 |
+ |
|
| 291 |
+remove_source_file() {
|
|
| 292 |
+ local src="$1" |
|
| 293 |
+ rm -f "$src" |
|
| 294 |
+} |
|
| 295 |
+ |
|
| 296 |
+copy_with_verification() {
|
|
| 297 |
+ local src="$1" |
|
| 298 |
+ local dst="$2" |
|
| 299 |
+ local expected_date="$3" |
|
| 300 |
+ |
|
| 301 |
+ if ! safe_cp "$src" "$dst"; then |
|
| 302 |
+ return 1 |
|
| 303 |
+ fi |
|
| 304 |
+ |
|
| 305 |
+ if ! verify_copied_file "$src" "$dst" "$expected_date"; then |
|
| 306 |
+ rm -f "$dst" |
|
| 307 |
+ return 1 |
|
| 308 |
+ fi |
|
| 309 |
+ |
|
| 310 |
+ return 0 |
|
| 311 |
+} |
|
| 312 |
+ |
|
| 313 |
+verified_move_file() {
|
|
| 314 |
+ local src="$1" |
|
| 315 |
+ local dst="$2" |
|
| 316 |
+ local expected_date="$3" |
|
| 317 |
+ |
|
| 318 |
+ if ! copy_with_verification "$src" "$dst" "$expected_date"; then |
|
| 319 |
+ return 1 |
|
| 320 |
+ fi |
|
| 321 |
+ |
|
| 322 |
+ if ! remove_source_file "$src"; then |
|
| 323 |
+ log_message "Copied and verified destination, but failed to remove source: $src" "ERROR" |
|
| 324 |
+ return 1 |
|
| 325 |
+ fi |
|
| 326 |
+ |
|
| 327 |
+ return 0 |
|
| 328 |
+} |
|
| 329 |
+ |
|
| 243 | 330 |
# Function to format file size |
| 244 | 331 |
format_size() {
|
| 245 | 332 |
local size=$1 |
@@ -533,10 +620,10 @@ process_file() {
|
||
| 533 | 620 |
if [[ $DRY_RUN -eq 1 ]]; then |
| 534 | 621 |
print_color "$BLUE" "Would move unsortable: $file -> $unsortable_path" |
| 535 | 622 |
else |
| 536 |
- if mv "$file" "$unsortable_path"; then |
|
| 623 |
+ if verified_move_file "$file" "$unsortable_path" ""; then |
|
| 537 | 624 |
log_message "Unsortable: $file -> $unsortable_path" "SUCCESS" |
| 538 | 625 |
else |
| 539 |
- log_message "Failed to move unsortable file: $file" "ERROR" |
|
| 626 |
+ log_message "Failed to move unsortable file after verification: $file" "ERROR" |
|
| 540 | 627 |
fi |
| 541 | 628 |
fi |
| 542 | 629 |
SKIPPED_FILES=$((SKIPPED_FILES + 1)) |
@@ -590,24 +677,24 @@ process_file() {
|
||
| 590 | 677 |
|
| 591 | 678 |
# Copy or move file using safe helpers (filter benign stderr). Let external tools handle renaming conflicts. |
| 592 | 679 |
if [[ $KEEP_ORIGINALS -eq 1 ]]; then |
| 593 |
- if safe_cp "$file" "$dest_path"; then |
|
| 680 |
+ if copy_with_verification "$file" "$dest_path" "$date_str"; then |
|
| 594 | 681 |
log_message "Copied: $file -> $dest_path" "SUCCESS" |
| 595 | 682 |
PROCESSED_FILES=$((PROCESSED_FILES + 1)) |
| 596 | 683 |
PROCESSED_SIZE=$((PROCESSED_SIZE + file_size)) |
| 597 | 684 |
return 0 |
| 598 | 685 |
else |
| 599 |
- log_message "Failed to copy: $file -> $dest_path" "ERROR" |
|
| 686 |
+ log_message "Failed to copy or verify destination: $file -> $dest_path" "ERROR" |
|
| 600 | 687 |
ERROR_FILES=$((ERROR_FILES + 1)) |
| 601 | 688 |
return 1 |
| 602 | 689 |
fi |
| 603 | 690 |
else |
| 604 |
- if safe_mv "$file" "$dest_path"; then |
|
| 691 |
+ if verified_move_file "$file" "$dest_path" "$date_str"; then |
|
| 605 | 692 |
log_message "Moved: $file -> $dest_path" "SUCCESS" |
| 606 | 693 |
PROCESSED_FILES=$((PROCESSED_FILES + 1)) |
| 607 | 694 |
PROCESSED_SIZE=$((PROCESSED_SIZE + file_size)) |
| 608 | 695 |
return 0 |
| 609 | 696 |
else |
| 610 |
- log_message "Failed to move: $file -> $dest_path" "ERROR" |
|
| 697 |
+ log_message "Failed to move after copy verification: $file -> $dest_path" "ERROR" |
|
| 611 | 698 |
ERROR_FILES=$((ERROR_FILES + 1)) |
| 612 | 699 |
return 1 |
| 613 | 700 |
fi |
@@ -705,6 +792,14 @@ while [[ $# -gt 0 ]]; do |
||
| 705 | 792 |
KEEP_ORIGINALS=1 |
| 706 | 793 |
shift |
| 707 | 794 |
;; |
| 795 |
+ --verify-mode) |
|
| 796 |
+ VERIFY_MODE="$2" |
|
| 797 |
+ if [[ ! "$VERIFY_MODE" =~ ^(size|strict|none)$ ]]; then |
|
| 798 |
+ print_color "$RED" "Error: Invalid verify mode. Must be one of: size, strict, none" |
|
| 799 |
+ exit 1 |
|
| 800 |
+ fi |
|
| 801 |
+ shift 2 |
|
| 802 |
+ ;; |
|
| 708 | 803 |
--dry-run) |
| 709 | 804 |
DRY_RUN=1 |
| 710 | 805 |
shift |
@@ -731,7 +826,24 @@ done |
||
| 731 | 826 |
|
| 732 | 827 |
# If no organization is provided, leave ORGANIZATION empty and filename mode will decide naming |
| 733 | 828 |
|
| 734 |
-# Set default destination: if user didn't provide -d and a source was given, use first source's directory + /sorted |
|
| 829 |
+# If no source specified, default to current directory. Refuse to run when cwd is unsafe. |
|
| 830 |
+if [[ ${#SOURCE_PATTERNS[@]} -eq 0 ]]; then
|
|
| 831 |
+ cwd=$(pwd) |
|
| 832 |
+ # Resolve home and root paths |
|
| 833 |
+ home_dir="$HOME" |
|
| 834 |
+ case "$cwd" in |
|
| 835 |
+ "$home_dir"|"/"|"/etc"|"/var"|"/bin"|"/sbin"|"/dev"|"/proc"|"/sys"|"/run"|"/usr"|"/lib"|"/lib64"|"/boot"|"/snap") |
|
| 836 |
+ print_color "$RED" "Refusing to run with default source in unsafe directory: $cwd" |
|
| 837 |
+ print_color "$RED" "Please specify $cwd as source explicitly with -s or run from a safer directory." |
|
| 838 |
+ exit 1 |
|
| 839 |
+ ;; |
|
| 840 |
+ *) |
|
| 841 |
+ SOURCE_PATTERNS+=("$cwd")
|
|
| 842 |
+ ;; |
|
| 843 |
+ esac |
|
| 844 |
+fi |
|
| 845 |
+ |
|
| 846 |
+# Set default destination: if user didn't provide -d and a source was given, use first source + /sorted |
|
| 735 | 847 |
if [[ -z "$DESTINATION" ]]; then |
| 736 | 848 |
if [[ ${#SOURCE_PATTERNS[@]} -gt 1 ]]; then
|
| 737 | 849 |
print_color "$RED" "Error: Multiple sources specified - destination (-d|--destination) is required when using multiple sources." |
@@ -740,18 +852,12 @@ if [[ -z "$DESTINATION" ]]; then |
||
| 740 | 852 |
fi |
| 741 | 853 |
|
| 742 | 854 |
if [[ ${#SOURCE_PATTERNS[@]} -gt 0 ]]; then
|
| 743 |
- # If exactly one source provided, place 'sorted' in the source's mount root |
|
| 744 | 855 |
first_source="${SOURCE_PATTERNS[0]}"
|
| 745 |
- if [[ -e "$first_source" ]]; then |
|
| 746 |
- mount_root=$(get_mountpoint "$first_source") |
|
| 747 |
- if [[ -n "$mount_root" ]]; then |
|
| 748 |
- DESTINATION="$mount_root/sorted" |
|
| 749 |
- else |
|
| 750 |
- # Fallback to dirname of source |
|
| 751 |
- DESTINATION="$(dirname "$first_source")/sorted" |
|
| 752 |
- fi |
|
| 856 |
+ if [[ -d "$first_source" ]]; then |
|
| 857 |
+ DESTINATION="$first_source/sorted" |
|
| 858 |
+ elif [[ -f "$first_source" ]]; then |
|
| 859 |
+ DESTINATION="$(dirname "$first_source")/sorted" |
|
| 753 | 860 |
else |
| 754 |
- # Source doesn't exist; fallback to ./sorted but warn |
|
| 755 | 861 |
print_color "$YELLOW" "Warning: first source does not exist; defaulting destination to ./sorted" |
| 756 | 862 |
DESTINATION="./sorted" |
| 757 | 863 |
fi |
@@ -760,23 +866,6 @@ if [[ -z "$DESTINATION" ]]; then |
||
| 760 | 866 |
fi |
| 761 | 867 |
fi |
| 762 | 868 |
|
| 763 |
-# If no source specified, default to current directory. Refuse to run when cwd is unsafe. |
|
| 764 |
-if [[ ${#SOURCE_PATTERNS[@]} -eq 0 ]]; then
|
|
| 765 |
- cwd=$(pwd) |
|
| 766 |
- # Resolve home and root paths |
|
| 767 |
- home_dir="$HOME" |
|
| 768 |
- case "$cwd" in |
|
| 769 |
- "$home_dir"|"/"|"/etc"|"/var"|"/bin"|"/sbin"|"/dev"|"/proc"|"/sys"|"/run"|"/usr"|"/lib"|"/lib64"|"/boot"|"/snap") |
|
| 770 |
- print_color "$RED" "Refusing to run with default source in unsafe directory: $cwd" |
|
| 771 |
- print_color "$RED" "Please specify $cwd as source explicitly with -s or run from a safer directory." |
|
| 772 |
- exit 1 |
|
| 773 |
- ;; |
|
| 774 |
- *) |
|
| 775 |
- SOURCE_PATTERNS+=("$cwd")
|
|
| 776 |
- ;; |
|
| 777 |
- esac |
|
| 778 |
-fi |
|
| 779 |
- |
|
| 780 | 869 |
# Convert destination to absolute path |
| 781 | 870 |
DESTINATION=$(cd "$(dirname "$DESTINATION")" 2>/dev/null && pwd)/$(basename "$DESTINATION") || DESTINATION=$(realpath "$DESTINATION" 2>/dev/null) || DESTINATION="$DESTINATION" |
| 782 | 871 |
|
@@ -787,6 +876,7 @@ echo "Configuration:" |
||
| 787 | 876 |
echo " Organization pattern: $ORGANIZATION" |
| 788 | 877 |
echo " Destination: $DESTINATION" |
| 789 | 878 |
echo " Keep originals: $([ $KEEP_ORIGINALS -eq 1 ] && echo "Yes" || echo "No")" |
| 879 |
+echo " Verify mode: $VERIFY_MODE" |
|
| 790 | 880 |
echo " Dry run: $([ $DRY_RUN -eq 1 ] && echo "Yes" || echo "No")" |
| 791 | 881 |
echo " Keep empty dirs: $([ $CLEANUP_EMPTY_DIRS -eq 0 ] && echo "Yes" || echo "No")" |
| 792 | 882 |
echo " Verbose: $([ $VERBOSE -eq 1 ] && echo "Yes" || echo "No")" |
@@ -496,6 +496,124 @@ run_source_only_test() {
|
||
| 496 | 496 |
"$result" |
| 497 | 497 |
} |
| 498 | 498 |
|
| 499 |
+# Test Case 8: Destination Inside Source Test |
|
| 500 |
+run_destination_inside_source_test() {
|
|
| 501 |
+ print_color "$GREEN" "=== Running Test 8: Destination Inside Source Test ===" |
|
| 502 |
+ |
|
| 503 |
+ clean_test_dir |
|
| 504 |
+ create_test_dirs |
|
| 505 |
+ |
|
| 506 |
+ # Setup test files |
|
| 507 |
+ copy_sample_files "media/sortable" "$SOURCE_DIR" |
|
| 508 |
+ |
|
| 509 |
+ # Capture pre-test state |
|
| 510 |
+ capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source" |
|
| 511 |
+ capture_state "$SOURCE_DIR/sorted" "$TEST_DIR/dest_before.txt" "destination-inside-source" |
|
| 512 |
+ |
|
| 513 |
+ # Run test with destination explicitly inside source |
|
| 514 |
+ local command="\"$MEDIA_IMPORTER\" -s \"$SOURCE_DIR\" -d \"$SOURCE_DIR/sorted\" -v" |
|
| 515 |
+ local result=0 |
|
| 516 |
+ run_test_command "$command" "$TEST_DIR/import_log.txt" || result=$? |
|
| 517 |
+ |
|
| 518 |
+ # Guard 1: no files should remain outside sorted for sortable-only dataset |
|
| 519 |
+ local outside_count |
|
| 520 |
+ outside_count=$(find "$SOURCE_DIR" -type f ! -path "$SOURCE_DIR/sorted/*" | wc -l | tr -d ' ') |
|
| 521 |
+ if [[ "$outside_count" != "0" ]]; then |
|
| 522 |
+ echo "Destination exclusion check failed: found $outside_count file(s) outside sorted" >> "$TEST_DIR/import_log.txt" |
|
| 523 |
+ result=1 |
|
| 524 |
+ fi |
|
| 525 |
+ |
|
| 526 |
+ # Guard 2: destination exclusion should prevent sorted/sorted recursion |
|
| 527 |
+ local recursive_sorted_count |
|
| 528 |
+ recursive_sorted_count=$(find "$SOURCE_DIR/sorted" -type d -path "*/sorted/sorted*" 2>/dev/null | wc -l | tr -d ' ') |
|
| 529 |
+ if [[ "$recursive_sorted_count" != "0" ]]; then |
|
| 530 |
+ echo "Destination recursion check failed: found nested sorted/sorted directories" >> "$TEST_DIR/import_log.txt" |
|
| 531 |
+ result=1 |
|
| 532 |
+ fi |
|
| 533 |
+ |
|
| 534 |
+ # Capture post-test state |
|
| 535 |
+ capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source" |
|
| 536 |
+ capture_state "$SOURCE_DIR/sorted" "$TEST_DIR/dest_after.txt" "destination-inside-source" |
|
| 537 |
+ |
|
| 538 |
+ # Generate report |
|
| 539 |
+ generate_test_report \ |
|
| 540 |
+ "Destination_Inside_Source" \ |
|
| 541 |
+ "Testing explicit destination inside source directory" \ |
|
| 542 |
+ "Verify destination exclusion and no sorted/sorted recursion" \ |
|
| 543 |
+ "Sortable sample media with destination set to source/sorted" \ |
|
| 544 |
+ "$command" \ |
|
| 545 |
+ "$result" |
|
| 546 |
+} |
|
| 547 |
+ |
|
| 548 |
+# Test Case 9: Verify Mode Test |
|
| 549 |
+run_verify_mode_test() {
|
|
| 550 |
+ print_color "$GREEN" "=== Running Test 9: Verify Mode Test ===" |
|
| 551 |
+ |
|
| 552 |
+ clean_test_dir |
|
| 553 |
+ create_test_dirs |
|
| 554 |
+ |
|
| 555 |
+ # Setup test files |
|
| 556 |
+ copy_sample_files "media/sortable" "$SOURCE_DIR" |
|
| 557 |
+ |
|
| 558 |
+ # Capture pre-test state |
|
| 559 |
+ capture_state "$SOURCE_DIR" "$TEST_DIR/source_before.txt" "source" |
|
| 560 |
+ capture_state "$SOURCE_DIR/sorted" "$TEST_DIR/dest_before.txt" "auto-destination" |
|
| 561 |
+ |
|
| 562 |
+ local log_file="$TEST_DIR/import_log.txt" |
|
| 563 |
+ local result=0 |
|
| 564 |
+ : > "$log_file" |
|
| 565 |
+ |
|
| 566 |
+ local command_default="cd \"$SOURCE_DIR\" && \"$MEDIA_IMPORTER\" --dry-run" |
|
| 567 |
+ local command_strict="cd \"$SOURCE_DIR\" && \"$MEDIA_IMPORTER\" --dry-run --verify-mode strict" |
|
| 568 |
+ local command="$command_default && $command_strict" |
|
| 569 |
+ |
|
| 570 |
+ echo "$command_default" >> "$log_file" |
|
| 571 |
+ echo "" >> "$log_file" |
|
| 572 |
+ echo "=== DEFAULT VERIFY MODE OUTPUT ===" >> "$log_file" |
|
| 573 |
+ if eval "$command_default" >> "$log_file" 2>&1; then |
|
| 574 |
+ if ! grep -q "Verify mode:[[:space:]]*size" "$log_file"; then |
|
| 575 |
+ echo "Expected default verify mode 'size' not found in output" >> "$log_file" |
|
| 576 |
+ result=1 |
|
| 577 |
+ fi |
|
| 578 |
+ else |
|
| 579 |
+ result=1 |
|
| 580 |
+ fi |
|
| 581 |
+ |
|
| 582 |
+ echo "" >> "$log_file" |
|
| 583 |
+ echo "$command_strict" >> "$log_file" |
|
| 584 |
+ echo "" >> "$log_file" |
|
| 585 |
+ echo "=== STRICT VERIFY MODE OUTPUT ===" >> "$log_file" |
|
| 586 |
+ if eval "$command_strict" >> "$log_file" 2>&1; then |
|
| 587 |
+ if ! grep -q "Verify mode:[[:space:]]*strict" "$log_file"; then |
|
| 588 |
+ echo "Expected strict verify mode not found in output" >> "$log_file" |
|
| 589 |
+ result=1 |
|
| 590 |
+ fi |
|
| 591 |
+ else |
|
| 592 |
+ result=1 |
|
| 593 |
+ fi |
|
| 594 |
+ |
|
| 595 |
+ if [[ $result -eq 0 ]]; then |
|
| 596 |
+ echo "" >> "$log_file" |
|
| 597 |
+ echo "=== COMMAND COMPLETED SUCCESSFULLY ===" >> "$log_file" |
|
| 598 |
+ else |
|
| 599 |
+ echo "" >> "$log_file" |
|
| 600 |
+ echo "=== COMMAND FAILED ===" >> "$log_file" |
|
| 601 |
+ fi |
|
| 602 |
+ |
|
| 603 |
+ # Capture post-test state |
|
| 604 |
+ capture_state "$SOURCE_DIR" "$TEST_DIR/source_after.txt" "source" |
|
| 605 |
+ capture_state "$SOURCE_DIR/sorted" "$TEST_DIR/dest_after.txt" "auto-destination" |
|
| 606 |
+ |
|
| 607 |
+ # Generate report |
|
| 608 |
+ generate_test_report \ |
|
| 609 |
+ "Verify_Mode" \ |
|
| 610 |
+ "Testing verify-mode defaults and strict override" \ |
|
| 611 |
+ "Verify default mode is size and strict mode is accepted" \ |
|
| 612 |
+ "Sample sortable media in source-only dry-run mode" \ |
|
| 613 |
+ "$command" \ |
|
| 614 |
+ "$result" |
|
| 615 |
+} |
|
| 616 |
+ |
|
| 499 | 617 |
# Function to show menu |
| 500 | 618 |
show_menu() {
|
| 501 | 619 |
echo "" |
@@ -511,10 +629,12 @@ show_menu() {
|
||
| 511 | 629 |
echo "5. Subdirectory Processing Test" |
| 512 | 630 |
echo "6. Keep Empty Directories Test" |
| 513 | 631 |
echo "7. Source Only Test" |
| 514 |
- echo "8. Run All Tests" |
|
| 632 |
+ echo "8. Destination Inside Source Test" |
|
| 633 |
+ echo "9. Verify Mode Test" |
|
| 634 |
+ echo "10. Run All Tests" |
|
| 515 | 635 |
echo "q. Quit" |
| 516 | 636 |
echo "" |
| 517 |
- echo -n "Select test to run (0-8, q to quit): " |
|
| 637 |
+ echo -n "Select test to run (0-10, q to quit): " |
|
| 518 | 638 |
} |
| 519 | 639 |
|
| 520 | 640 |
# Function to run all tests |
@@ -536,6 +656,10 @@ run_all_tests() {
|
||
| 536 | 656 |
run_keep_empty_dirs_test |
| 537 | 657 |
echo "" |
| 538 | 658 |
run_source_only_test |
| 659 |
+ echo "" |
|
| 660 |
+ run_destination_inside_source_test |
|
| 661 |
+ echo "" |
|
| 662 |
+ run_verify_mode_test |
|
| 539 | 663 |
|
| 540 | 664 |
print_color "$GREEN" "All tests completed!" |
| 541 | 665 |
} |
@@ -572,6 +696,12 @@ main() {
|
||
| 572 | 696 |
run_source_only_test |
| 573 | 697 |
;; |
| 574 | 698 |
8) |
| 699 |
+ run_destination_inside_source_test |
|
| 700 |
+ ;; |
|
| 701 |
+ 9) |
|
| 702 |
+ run_verify_mode_test |
|
| 703 |
+ ;; |
|
| 704 |
+ 10) |
|
| 575 | 705 |
run_all_tests |
| 576 | 706 |
;; |
| 577 | 707 |
q|Q) |
@@ -579,7 +709,7 @@ main() {
|
||
| 579 | 709 |
exit 0 |
| 580 | 710 |
;; |
| 581 | 711 |
*) |
| 582 |
- print_color "$RED" "Invalid choice. Please select 0-8 or q to quit." |
|
| 712 |
+ print_color "$RED" "Invalid choice. Please select 0-10 or q to quit." |
|
| 583 | 713 |
;; |
| 584 | 714 |
esac |
| 585 | 715 |
|
@@ -630,7 +760,13 @@ else |
||
| 630 | 760 |
"source-only"|"7") |
| 631 | 761 |
run_source_only_test |
| 632 | 762 |
;; |
| 633 |
- "all"|"8") |
|
| 763 |
+ "dest-in-source"|"8") |
|
| 764 |
+ run_destination_inside_source_test |
|
| 765 |
+ ;; |
|
| 766 |
+ "verify-mode"|"9") |
|
| 767 |
+ run_verify_mode_test |
|
| 768 |
+ ;; |
|
| 769 |
+ "all"|"10") |
|
| 634 | 770 |
run_all_tests |
| 635 | 771 |
;; |
| 636 | 772 |
"clean") |
@@ -641,7 +777,7 @@ else |
||
| 641 | 777 |
;; |
| 642 | 778 |
*) |
| 643 | 779 |
print_color "$RED" "Unknown test: $1" |
| 644 |
- echo "Usage: $0 [basic|unimportable|mixed|safety|utc|subdir|keep-empty|source-only|all|clean|setup] or [0-8]" |
|
| 780 |
+ echo "Usage: $0 [basic|unimportable|mixed|safety|utc|subdir|keep-empty|source-only|dest-in-source|verify-mode|all|clean|setup] or [0-10]" |
|
| 645 | 781 |
exit 1 |
| 646 | 782 |
;; |
| 647 | 783 |
esac |