# Dashcam Transfer Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Automatic transfer of dashcam MP4 files from SD card to USB-SSD. **Architecture:** Single Python script (`dashcam-transfer.py`) using stdlib only, triggered by udev rule via systemd oneshot service. **Tech Stack:** Python 3 (stdlib), systemd, udev --- ### Task 1: Write dashcam-transfer.py **Files:** - Create: `dashcam-transfer.py` (local repo) - Deploy to: `/usr/local/bin/dashcam-transfer` on nextcloud The script must: 1. Parse all `.MP4` files from `/mnt/sdcard/Novatek/Movie/` 2. Extract date (first 8 chars of filename) → `YYYYMMDD/` dir 3. Extract camera suffix (last char before `.MP4`): - `F` → `front/` - `B` → `back/` 4. Create target dirs: `/mnt/ultratouch/dashcam/YYYYMMDD/{front,back}/` 5. Calculate total space needed, check available space 6. If insufficient: sort date dirs on ultratouch by name, delete oldest dirs until enough space 7. `mv` each file (not copy) 8. Log every action to `/var/log/dashcam-transfer.log` 9. Use `flock` lockfile to prevent concurrent runs 10. Error handling: skip unreadable files, abort if SD not mounted - [ ] **Write dashcam-transfer.py** ```python #!/usr/bin/env python3 """Transfer dashcam MP4 files from SD card to USB-SSD with date/camera structure.""" import fcntl import logging import os import re import shutil import sys # Config SD_CARD = "/mnt/sdcard" SOURCE_DIR = os.path.join(SD_CARD, "Novatek", "Movie") SSD = "/mnt/ultratouch" TARGET_BASE = os.path.join(SSD, "dashcam") LOG_FILE = "/var/log/dashcam-transfer.log" LOCK_FILE = "/var/run/dashcam-transfer.lock" CAMERA_MAP = {"F": "front", "B": "back"} FILE_PATTERN = re.compile(r"^(\d{8})\d{6}_\d+([FB])\.MP4$") logging.basicConfig( filename=LOG_FILE, level=logging.INFO, format="[%(asctime)s] %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) def acquire_lock(): lock_fd = open(LOCK_FILE, "w") try: fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) except IOError: logging.warning("Lockfile actief — script al bezig") sys.exit(1) return lock_fd def check_mount(path, name): if not os.path.ismount(path): logging.error("%s niet gemount: %s", name, path) sys.exit(1) def get_mp4_files(src_dir): if not os.path.isdir(src_dir): logging.info("Geen bronmap: %s", src_dir) return [] files = [] for fname in os.listdir(src_dir): match = FILE_PATTERN.match(fname) if match: files.append((fname, match.group(1), match.group(2).upper())) return files def ensure_free_space(needed_bytes, target_base): stat = os.statvfs(target_base) free = stat.f_frsize * stat.f_bavail if free >= needed_bytes: return logging.info("Onvoldoende ruimte: %d nodig, %d vrij", needed_bytes, free) date_dirs = sorted( d for d in os.listdir(target_base) if re.match(r"^\d{8}$", d) and os.path.isdir(os.path.join(target_base, d)) ) freed = 0 removed = [] for d in date_dirs: if free + freed >= needed_bytes: break dir_path = os.path.join(target_base, d) dir_size = sum( os.path.getsize(os.path.join(dirpath, f)) for dirpath, _, filenames in os.walk(dir_path) for f in filenames ) shutil.rmtree(dir_path) freed += dir_size removed.append(d) logging.info("Verwijderd (ruimte gebrek): %s (%d bytes)", d, dir_size) if free + freed < needed_bytes: logging.error( "Nog steeds onvoldoende ruimte na verwijderen %d mappen", len(removed) ) sys.exit(1) logging.info("Ruimte vrijgemaakt: %d bytes door %d mappen", freed, len(removed)) def main(): lock_fd = acquire_lock() check_mount(SD_CARD, "SD-kaart") check_mount(SSD, "Ultratouch SSD") files = get_mp4_files(SOURCE_DIR) if not files: logging.info("Geen MP4 bestanden gevonden in %s", SOURCE_DIR) return total_size = sum( os.path.getsize(os.path.join(SOURCE_DIR, fname)) for fname, _, _ in files ) logging.info( "Start transfer: %d bestanden, %d bytes", len(files), total_size ) ensure_free_space(total_size, TARGET_BASE) ok = 0 errors = 0 for fname, date_str, cam in files: target_dir = os.path.join(TARGET_BASE, date_str, CAMERA_MAP[cam]) os.makedirs(target_dir, exist_ok=True) src = os.path.join(SOURCE_DIR, fname) dst = os.path.join(target_dir, fname) try: shutil.move(src, dst) logging.info("Verplaatst: %s → %s/%s/", fname, date_str, CAMERA_MAP[cam]) ok += 1 except OSError as e: logging.error("Fout bij %s: %s", fname, e) errors += 1 logging.info( "Transfer voltooid: %d ok, %d fouten, %d bytes", ok, errors, total_size ) if __name__ == "__main__": main() ``` - [ ] **Commit to repo** ```bash git add dashcam-transfer.py git commit -m "feat: dashcam-transfer.py script" git push ``` --- ### Task 2: Deploy and test on nextcloud **Files:** - Modify: `/usr/local/bin/dashcam-transfer` on nextcloud - [ ] **Copy script to nextcloud** ```bash scp dashcam-transfer.py aiaiaiaiagent@nextcloud:/tmp/ ssh aiaiaiaiagent@nextcloud 'sudo -A install -m 755 /tmp/dashcam-transfer.py /usr/local/bin/dashcam-transfer' ``` - [ ] **Dry-run test (check detection, no transfer yet)** ```bash ssh aiaiaiaiagent@nextcloud '/usr/local/bin/dashcam-transfer' ``` Expected: Script should find 338 MP4 files, log the start, and proceed with transfer. --- ### Task 3: Add udev rule + systemd service (if needed) **Files:** - Create: `/etc/udev/rules.d/99-dashcam-sdcard.rules` on nextcloud - Create: `/etc/systemd/system/dashcam-transfer.service` on nextcloud - Create: `dashcam-transfer-trigger` script on nextcloud - [ ] **Create trigger script** ```bash ssh aiaiaiaiagent@nextcloud 'sudo tee /usr/local/bin/dashcam-transfer-trigger << "TRIGGER" #!/bin/bash sleep 5 /usr/local/bin/dashcam-transfer TRIGGER' ssh aiaiaiaiagent@nextcloud 'sudo -A chmod +x /usr/local/bin/dashcam-transfer-trigger' ``` - [ ] **Create systemd service** ```ini [Unit] Description=Dashcam SD card transfer After=mnt-sdcard.mount [Service] Type=oneshot ExecStart=/usr/local/bin/dashcam-transfer ``` - [ ] **Create udev rule** ```udev ACTION=="add", KERNEL=="sdd1", RUN+="/usr/local/bin/dashcam-transfer-trigger" ```