@@ -0,0 +1,96 @@ |
||
| 1 |
+# Agent README |
|
| 2 |
+ |
|
| 3 |
+## Scope |
|
| 4 |
+ |
|
| 5 |
+Acest fisier este pentru agentii si toolurile care folosesc acest repository ca baza pentru importuri media automate, inclusiv rulari unattended din udev, systemd, cron, pipeline-uri NAS sau wrapper-e generate. |
|
| 6 |
+ |
|
| 7 |
+Scriptul principal validat este: |
|
| 8 |
+ |
|
| 9 |
+```text |
|
| 10 |
+media-importer.sh |
|
| 11 |
+``` |
|
| 12 |
+ |
|
| 13 |
+Prototipurile Raspberry Pi / unattended wrapper au fost eliminate din repository deoarece erau bazate pe o versiune invechita si netestata a importerului. Nu le recreati si nu le folositi ca reference design fara o decizie explicita de reluare a acelei directii. |
|
| 14 |
+ |
|
| 15 |
+## Context critic: incidente de pierdere de date |
|
| 16 |
+ |
|
| 17 |
+Au existat pierderi reale de date in mediul nostru cu versiuni anterioare ale importerului. Incidentul documentat in `INCIDENTS.md` a fost declansat de fisiere GoPro chapter care au ajuns sa fie mapate la acelasi nume destinatie. Scriptul vechi a continuat dupa conflict, copia/mutarea a suprascris destinatia existenta, verificarea a validat fisierul suprascris, iar sursele au fost sterse. |
|
| 18 |
+ |
|
| 19 |
+Orice agent care copiaza, deriva, rescrie sau integreaza acest script intr-un tool unattended trebuie sa verifice explicit ca nu reintroduce bugurile care au dus la acea pierdere de date. |
|
| 20 |
+ |
|
| 21 |
+## Reguli obligatorii pentru import unattended |
|
| 22 |
+ |
|
| 23 |
+- Nu folositi variante vechi, copii locale sau scripturi derivate fara comparatie cu protectiile curente din `media-importer.sh`. |
|
| 24 |
+- Nu folositi `cp`, `mv`, `rsync` sau alte unelte lasate sa rezolve singure conflictele de destinatie. Conflictele trebuie rezolvate in cod inainte de operatie. |
|
| 25 |
+- Nu suprascrieti niciodata o destinatie existenta. |
|
| 26 |
+- Nu stergeti sursa pana cand destinatia nu exista si verificarea a trecut. |
|
| 27 |
+- In move mode, fluxul sigur este `copy -> verify -> delete source`; nu inlocuiti acest flux cu `mv` direct in scripturi unattended. |
|
| 28 |
+- Destinatiile planificate in aceeasi rulare trebuie rezervate in memorie, ca doua surse sa nu poata ajunge la acelasi path chiar daca fisierul destinatie nu exista inca. |
|
| 29 |
+- Pentru conflicte unattended, comportamentul acceptat este suffix numeric unic (`_1`, `_2`, ...), skip explicit sau abort. Nu este acceptat overwrite silentios. |
|
| 30 |
+- Daca destinatia este in interiorul sursei, destinatia trebuie exclusa din scanare, iar orice fisier aflat deja in destinatie trebuie refuzat inainte de procesare. |
|
| 31 |
+- Pentru camere GoPro/Garmin/Varia sau fisiere QuickTime, nu modificati logica de data/timp fara teste de regresie; timestampurile identice sunt un caz normal, nu o eroare de intrare. |
|
| 32 |
+ |
|
| 33 |
+## Checklist pentru copii sau scripturi derivate |
|
| 34 |
+ |
|
| 35 |
+Inainte ca o copie sau derivare sa fie folosita unattended, verificati ca include echivalentul acestor protectii: |
|
| 36 |
+ |
|
| 37 |
+- `safe_cp` si `safe_mv` refuza destinatii existente. |
|
| 38 |
+- Copierea se face intr-un fisier temporar, se verifica temporarul, apoi se muta in destinatia finala doar daca destinatia finala nu exista. |
|
| 39 |
+- `VERIFY_MODE=size` este default; `strict` este disponibil pentru comparatie byte-to-byte. |
|
| 40 |
+- Stergerea sursei are loc doar dupa verificarea destinatiei. |
|
| 41 |
+- Conflictele de destinatie sunt rezolvate prin `resolve_destination_conflict` / `ensure_unique_destination_path` sau logica echivalenta. |
|
| 42 |
+- Path-urile destinatie deja planificate sunt rezervate, nu doar cele existente pe disk. |
|
| 43 |
+- Testul `timestamp-collision` trece. |
|
| 44 |
+- Dry-run-ul pentru camere noi arata destinatii distincte pentru fiecare fisier. |
|
| 45 |
+ |
|
| 46 |
+## Teste minime inainte de automatizare |
|
| 47 |
+ |
|
| 48 |
+Pentru orice schimbare care atinge importul, copierea, mutarea, verificarea, stergerea sursei, generarea numelor sau extragerea datelor, rulati cel putin: |
|
| 49 |
+ |
|
| 50 |
+```bash |
|
| 51 |
+./test_runner.sh timestamp-collision |
|
| 52 |
+./test_runner.sh verify-mode |
|
| 53 |
+./test_runner.sh dest-in-source |
|
| 54 |
+``` |
|
| 55 |
+ |
|
| 56 |
+Pentru schimbari legate de GoPro sau metadata sidecar, rulati si: |
|
| 57 |
+ |
|
| 58 |
+```bash |
|
| 59 |
+./test_runner.sh gopro-sidecar-sync |
|
| 60 |
+./test_runner.sh gopro-no-sidecar-reimport |
|
| 61 |
+``` |
|
| 62 |
+ |
|
| 63 |
+Daca aceste teste nu pot fi rulate, agentul trebuie sa marcheze integrarea ca nevalidata si sa nu recomande unattended move mode. |
|
| 64 |
+ |
|
| 65 |
+## Recomandari operationale |
|
| 66 |
+ |
|
| 67 |
+Pentru camere sau destinatii noi, prima rulare trebuie sa fie dry-run: |
|
| 68 |
+ |
|
| 69 |
+```bash |
|
| 70 |
+./media-importer.sh -s "/path/to/camera" -d "/path/to/destination" --dry-run -v |
|
| 71 |
+``` |
|
| 72 |
+ |
|
| 73 |
+Verificati in output ca: |
|
| 74 |
+ |
|
| 75 |
+- fiecare fisier sursa are o destinatie distincta; |
|
| 76 |
+- nu exista mesaje de overwrite; |
|
| 77 |
+- conflictele sunt suffixate, sarite explicit sau duc la abort; |
|
| 78 |
+- pentru GoPro, sursa datei este cea asteptata, inclusiv sidecar `THM`/`LRV` cand exista. |
|
| 79 |
+ |
|
| 80 |
+Pentru date cu valoare mare sau cand sursa poate fi pastrata, preferati temporar: |
|
| 81 |
+ |
|
| 82 |
+```bash |
|
| 83 |
+./media-importer.sh -s "/path/to/camera" -d "/path/to/destination" --verify-mode strict --keep-originals -v |
|
| 84 |
+``` |
|
| 85 |
+ |
|
| 86 |
+## Regula de oprire |
|
| 87 |
+ |
|
| 88 |
+Daca un agent gaseste o copie a scriptului care: |
|
| 89 |
+ |
|
| 90 |
+- foloseste `mv` direct in move mode; |
|
| 91 |
+- permite overwrite; |
|
| 92 |
+- sterge sursa fara verificare; |
|
| 93 |
+- nu rezerva destinatii planificate; |
|
| 94 |
+- sau nu poate demonstra ca trece testul `timestamp-collision`; |
|
| 95 |
+ |
|
| 96 |
+atunci agentul trebuie sa opreasca integrarea unattended si sa raporteze riscul ca posibil bug de pierdere de date. |
|
@@ -5,10 +5,9 @@ This repository contains the development and testing resources for the `media-im |
||
| 5 | 5 |
## Project Structure |
| 6 | 6 |
|
| 7 | 7 |
- `media-importer.sh`: The primary script under development and testing. |
| 8 |
+- `AGENTS.md`: Safety rules for agents or unattended automation that copy, derive, or run the importer. |
|
| 8 | 9 |
- `INCIDENTS.md`: Postmortems for data-loss or near-data-loss incidents and the regression rules derived from them. |
| 9 |
-- `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. |
|
| 10 |
-- `samples/code/autonas-media-importer.sh`: The original script that serves as inspiration for this project. |
|
| 11 |
-- `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. |
|
| 10 |
+- `sample/`: Contains local media fixtures used for development and testing. **Note:** Scripts and functions are not imported from sample directories. |
|
| 12 | 11 |
|
| 13 | 12 |
## Version Control |
| 14 | 13 |
|
@@ -23,8 +22,9 @@ samples/ |
||
| 23 | 22 |
## Contribution Guidelines |
| 24 | 23 |
|
| 25 | 24 |
- All development should focus on `media-importer.sh`. |
| 26 |
-- Do not import or source scripts/functions from the `samples/` directory. |
|
| 27 |
-- Use resources in `samples/` only for testing and development reference. |
|
| 25 |
+- Do not import, source, or derive importer behavior from sample directories. |
|
| 26 |
+- Use sample media only for testing and development reference. |
|
| 27 |
+- Do not use removed Raspberry Pi / unattended wrapper prototypes as a reference design; they were based on an outdated, unvalidated importer copy. |
|
| 28 | 28 |
- Destination conflicts must never be delegated to copy/move tools. In unattended runs the importer appends numeric suffixes (`_1`, `_2`, ...); in interactive runs it asks the user and supports applying the choice to all similar conflicts. |
| 29 | 29 |
|
| 30 | 30 |
## License |
@@ -1,15 +0,0 @@ |
||
| 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. |
|
@@ -1,46 +0,0 @@ |
||
| 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 |
-``` |
|
@@ -1,103 +0,0 @@ |
||
| 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` |
|
@@ -1,26 +0,0 @@ |
||
| 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}"
|
|
@@ -1,4 +0,0 @@ |
||
| 1 |
-# Camera profiles for rpi-camera-importer |
|
| 2 |
-# Format: name|uuid|destination_path |
|
| 3 |
-# Example: |
|
| 4 |
-# varia_rct715|A1B2-C3D4|/srv/media/varia |
|
@@ -1,14 +0,0 @@ |
||
| 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 |
|
@@ -1,438 +0,0 @@ |
||
| 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 |
|
@@ -1,59 +0,0 @@ |
||
| 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 |
|
@@ -1,676 +0,0 @@ |
||
| 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 "$@" |
|
@@ -1,39 +0,0 @@ |
||
| 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 |
|
@@ -1,63 +0,0 @@ |
||
| 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 "$@" |
|