commit 6ab601771cb23464b69e3111041511b8b2b54a5b Author: RandyJC Date: Mon Dec 1 09:24:53 2025 +0100 pushing files diff --git a/.env b/.env new file mode 100644 index 0000000..c166842 --- /dev/null +++ b/.env @@ -0,0 +1,23 @@ +# User/permissions +PUID=99 +PGID=100 +TZ=UTC + +# Paths on the host (Unraid shares) +INBOX=/mnt/user/photosync/inbox +DROP=/mnt/user/photosync/drop + +# Immich connection +IMMICH_URL=http://immich:8080 +IMMICH_API_KEY=REPLACE_ME +ALLOW_INSECURE_SSL=false + +# Mover tuning +STABLE_FOR=5 +SCAN_FALLBACK=60 + +# Uploader tuning +UPLOAD_CONCURRENCY=4 +UPLOAD_SCAN_INTERVAL=5 +UPLOAD_IDLE_SLEEP=3 +DELETE_ON_SUCCESS=true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c977352 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: "3.9" + +services: + photosync-mover: + image: alpine:3.19 + container_name: photosync-mover + restart: unless-stopped + environment: + - TZ=${TZ:-UTC} + - INBOX=${INBOX} + - DROP=${DROP} + - STABLE_FOR=${STABLE_FOR:-5} + - SCAN_FALLBACK=${SCAN_FALLBACK:-60} + volumes: + - ${INBOX}:/inbox:rw + - ${DROP}:/drop:rw + - ./move_stable.sh:/scripts/move_stable.sh:ro + entrypoint: ["/bin/sh","-c","apk add --no-cache inotify-tools coreutils >/dev/null && chmod +x /scripts/move_stable.sh && exec /scripts/move_stable.sh"] + user: "${PUID:-1000}:${PGID:-1000}" + + photosync-uploader: + image: ghcr.io/immich-app/immich-go:release + container_name: photosync-uploader + restart: unless-stopped + depends_on: + - photosync-mover + environment: + - TZ=${TZ:-UTC} + - IMMICH_URL=${IMMICH_URL} + - IMMICH_API_KEY=${IMMICH_API_KEY} + - SCAN_DIR=/drop + - SCAN_INTERVAL=${UPLOAD_SCAN_INTERVAL:-5} + - IDLE_SLEEP=${UPLOAD_IDLE_SLEEP:-3} + - CONCURRENCY=${UPLOAD_CONCURRENCY:-4} + - DELETE_ON_SUCCESS=${DELETE_ON_SUCCESS:-true} + - ALLOW_INSECURE_SSL=${ALLOW_INSECURE_SSL:-false} + volumes: + - ${DROP}:/drop:rw + - ./uploader.sh:/scripts/uploader.sh:ro + entrypoint: ["/bin/sh","-c","chmod +x /scripts/uploader.sh && exec /scripts/uploader.sh"] + user: "${PUID:-1000}:${PGID:-1000}" diff --git a/monitor.sh b/monitor.sh new file mode 100755 index 0000000..8c1204a --- /dev/null +++ b/monitor.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -eu + +INBOX="${INBOX:-/mnt/user/photosync/inbox}" +DROP="${DROP:-/mnt/user/photosync/drop}" +LOGFILE="${LOGFILE:-/mnt/user/photosync-scripts/monitor.log}" +MOVER_CONTAINER="${MOVER_CONTAINER:-photosync-mover}" +UPLOADER_CONTAINER="${UPLOADER_CONTAINER:-photosync-uploader}" +TAIL_LINES="${TAIL_LINES:-10}" +TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')" + +echo "=== Photosync Monitor @ $TIMESTAMP ===" | tee -a "$LOGFILE" + +inbox_count=$(find "$INBOX" -type f 2>/dev/null | wc -l | tr -d ' ') +drop_count=$(find "$DROP" -type f 2>/dev/null | wc -l | tr -d ' ') +echo "Inbox files: $inbox_count" | tee -a "$LOGFILE" +echo "Drop files: $drop_count" | tee -a "$LOGFILE" + +for svc in "$MOVER_CONTAINER" "$UPLOADER_CONTAINER"; do + if docker ps --format '{{.Names}}' | grep -q "^$svc$"; then + echo "Container $svc: RUNNING" | tee -a "$LOGFILE" + else + echo "Container $svc: NOT RUNNING" | tee -a "$LOGFILE" + fi +done + +echo "--- Last $TAIL_LINES uploader log lines ---" | tee -a "$LOGFILE" +docker logs --tail "$TAIL_LINES" "$UPLOADER_CONTAINER" 2>&1 | tee -a "$LOGFILE" +echo "" | tee -a "$LOGFILE" diff --git a/move_stable.sh b/move_stable.sh new file mode 100755 index 0000000..8e7c43b --- /dev/null +++ b/move_stable.sh @@ -0,0 +1,65 @@ +#!/bin/sh +set -eu + +INBOX="${INBOX:-/inbox}" +DROP="${DROP:-/drop}" +STABLE_FOR="${STABLE_FOR:-5}" # seconds with unchanged size to consider stable +SCAN_FALLBACK="${SCAN_FALLBACK:-60}" # seconds between full scans (safety net) + +echo "[mover] Watching inbox ${INBOX} -> ${DROP} (stable for ${STABLE_FOR}s)" + +mkdir -p "$INBOX" "$DROP" + +is_stable() { + f="$1" + [ -f "$f" ] || return 1 + size_a="$(stat -c%s "$f" 2>/dev/null || echo 0)" + sleep "$STABLE_FOR" + size_b="$(stat -c%s "$f" 2>/dev/null || echo 0)" + [ "$size_a" -eq "$size_b" ] +} + +move_file() { + src="$1" + [ -f "$src" ] || return 0 + rel="${src#$INBOX/}" + dest_dir="$DROP/$(dirname "$rel")" + dest="$dest_dir/$(basename "$rel")" + mkdir -p "$dest_dir" + mv -f -- "$src" "$dest" + echo "[mover] Moved $(basename "$src")" +} + +process_one() { + f="$1" + [ -f "$f" ] || return 0 + if is_stable "$f"; then + move_file "$f" + fi +} + +scan_all() { + find "$INBOX" -type f -size +0c -print0 | while IFS= read -r -d '' f; do + process_one "$f" + done +} + +start_watcher() { + if command -v inotifywait >/dev/null 2>&1; then + echo "[mover] Starting inotify watcher" + inotifywait -q -m -r -e close_write,moved_to,create --format '%w%f' "$INBOX" | \ + while IFS= read -r path; do + process_one "$path" + done & + echo "[mover] Watcher pid $!" + else + echo "[mover] inotifywait not available; relying on periodic scans" + fi +} + +start_watcher + +while :; do + scan_all + sleep "$SCAN_FALLBACK" +done diff --git a/uploader.sh b/uploader.sh new file mode 100755 index 0000000..0fdd51f --- /dev/null +++ b/uploader.sh @@ -0,0 +1,113 @@ +#!/bin/sh +set -eu + +IMMICH_URL="${IMMICH_URL:?IMMICH_URL is required}" # e.g. http://immich:8080 +IMMICH_API_KEY="${IMMICH_API_KEY:?IMMICH_API_KEY is required}" +SCAN_DIR="${SCAN_DIR:-/drop}" +DELETE_ON_SUCCESS="${DELETE_ON_SUCCESS:-true}" +SCAN_INTERVAL="${SCAN_INTERVAL:-5}" +ALLOW_INSECURE_SSL="${ALLOW_INSECURE_SSL:-false}" +CONCURRENCY="${CONCURRENCY:-4}" +IDLE_SLEEP="${IDLE_SLEEP:-3}" + +case "$CONCURRENCY" in + ''|*[!0-9]*) CONCURRENCY=1 ;; +esac +[ "$CONCURRENCY" -lt 1 ] && CONCURRENCY=1 + +echo "[uploader] Target: ${IMMICH_URL}" +echo "[uploader] Source (drop): ${SCAN_DIR} (scan every ${SCAN_INTERVAL}s, concurrency ${CONCURRENCY})" + +CURL_OPTS="-sS -m 5 -o /dev/null -w %{http_code}" +[ "$ALLOW_INSECURE_SSL" = "true" ] && CURL_OPTS="$CURL_OPTS -k" +PING="${IMMICH_URL%/}/api/server-info" + +wait_for_immich() { + backoff=1 + max_backoff=30 + last="" + while :; do + code="$(curl $CURL_OPTS "$PING" 2>/dev/null || true)" + case "$code" in + 2*|3*|4*) [ "$last" != "up" ] && echo "[uploader] Immich is up (HTTP $code)."; return 0 ;; + *) [ "$last" != "down" ] && echo "[uploader] Waiting for Immich at $PING ...";; + esac + last="down" + sleep "$backoff" + [ "$backoff" -lt "$max_backoff" ] && backoff=$((backoff*2)) + [ "$backoff" -gt "$max_backoff" ] && backoff="$max_backoff" + done +} + +prune_empty_parents() { + dir="$(dirname "$1")" + while [ "$dir" != "$SCAN_DIR" ]; do + rmdir "$dir" 2>/dev/null || break + dir="$(dirname "$dir")" + done +} + +upload_one() { + f="$1" + if immich-go upload from-folder \ + --server "${IMMICH_URL}" \ + --api-key "${IMMICH_API_KEY}" \ + --no-ui --on-server-errors continue \ + --pause-immich-jobs=false \ + --recursive=false \ + "$f" >/dev/null 2>&1 + then + echo "[uploader] OK: $(basename "$f")" + if [ "$DELETE_ON_SUCCESS" = "true" ]; then + rm -f -- "$f" || true + prune_empty_parents "$f" + fi + else + echo "[uploader] FAIL: $(basename "$f") — will retry next loop" + fi +} + +upload_batch() { + count="$#" + echo "[uploader] Found $count file(s) to upload" + running=0 + pids="" + + for f in "$@"; do + upload_one "$f" & + pid=$! + pids="$pids $pid" + running=$((running+1)) + + if [ "$running" -ge "$CONCURRENCY" ]; then + set -- $pids + pid_to_wait="$1" + pids="${pids# $pid_to_wait}" + wait "$pid_to_wait" || true + running=$((running-1)) + fi + done + + for pid in $pids; do + wait "$pid" || true + done +} + +while :; do + wait_for_immich + + file_list="$(find "$SCAN_DIR" -type f -size +0c -print0 | xargs -0 -I{} printf '%s\n' "{}")" + if [ -z "$file_list" ]; then + sleep "$IDLE_SLEEP" + continue + fi + + oldifs="$IFS" + IFS=' +' + set -- $file_list + IFS="$oldifs" + + upload_batch "$@" + sleep "$SCAN_INTERVAL" +done