From f9164b4fa0eb528f5e57072b00e017515a6a44ab Mon Sep 17 00:00:00 2001 From: tocmo Date: Sun, 5 Apr 2026 01:42:45 -0400 Subject: [PATCH] Add Debian package and Gitea APT repository support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit debian/control, postinst, prerm, postrm — standard dpkg package lifecycle debian/files/opt/dupfinder/dupfinder-setup.sh — interactive setup: checks Docker, detects NVIDIA GPU, prompts for photos/data paths, writes docker-compose.override.yml with GPU reservation if present, pulls image from registry (builds from source as fallback) debian/files/usr/local/bin/dupfinder — CLI wrapper: setup / start / stop / restart / status / logs / open / update debian/files/etc/systemd/system/dupfinder.service — systemd unit, guards against starting before setup has run debian/build-deb.sh — builds .deb and uploads to Gitea package registry; prints the exact apt sources.list line on success Install on any Debian/Ubuntu machine: echo "deb [trusted=yes] http://192.168.1.64:3000/api/packages/tocmo0nlord/debian bookworm main" \ | sudo tee /etc/apt/sources.list.d/dupfinder.list sudo apt update && sudo apt install dupfinder sudo dupfinder setup Co-Authored-By: Claude Sonnet 4.6 --- debian/build-deb.sh | 141 +++++++++++++++ debian/control | 15 ++ .../etc/systemd/system/dupfinder.service | 33 ++++ debian/files/opt/dupfinder/dupfinder-setup.sh | 167 ++++++++++++++++++ debian/files/usr/local/bin/dupfinder | 81 +++++++++ debian/postinst | 27 +++ debian/postrm | 13 ++ debian/prerm | 5 + 8 files changed, 482 insertions(+) create mode 100644 debian/build-deb.sh create mode 100644 debian/control create mode 100644 debian/files/etc/systemd/system/dupfinder.service create mode 100644 debian/files/opt/dupfinder/dupfinder-setup.sh create mode 100644 debian/files/usr/local/bin/dupfinder create mode 100644 debian/postinst create mode 100644 debian/postrm create mode 100644 debian/prerm diff --git a/debian/build-deb.sh b/debian/build-deb.sh new file mode 100644 index 0000000..b8624ca --- /dev/null +++ b/debian/build-deb.sh @@ -0,0 +1,141 @@ +#!/bin/bash +# Build dupfinder.deb and upload it to the Gitea package registry. +# Run this on the NAS / any Linux machine with dpkg-deb and curl installed. +# +# Usage: +# ./debian/build-deb.sh +# ./debian/build-deb.sh --no-upload # build only, skip Gitea upload +set -e + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DEBIAN_DIR="$REPO_ROOT/debian" +BUILD_DIR="$REPO_ROOT/build/deb" + +# ── Config ──────────────────────────────────────────────────────────────────── +PKG_NAME="dupfinder" +PKG_VERSION="1.0.0" +PKG_ARCH="amd64" +DEB_FILE="${PKG_NAME}_${PKG_VERSION}_${PKG_ARCH}.deb" + +GITEA_URL="http://192.168.1.64:3000" +GITEA_OWNER="tocmo0nlord" +GITEA_TOKEN="${GITEA_TOKEN:-72d5ea48d6b97d2185a774de0a36c88ae30f91b0}" +DISTRO="bookworm" +COMPONENT="main" + +NO_UPLOAD=false +[[ "${1}" == "--no-upload" ]] && NO_UPLOAD=true + +# ── Helpers ─────────────────────────────────────────────────────────────────── +info() { echo -e "\033[0;36m==> $*\033[0m"; } +ok() { echo -e "\033[0;32m OK $*\033[0m"; } +fail() { echo -e "\033[0;31m !! $*\033[0m"; exit 1; } + +# ── Check dependencies ──────────────────────────────────────────────────────── +command -v dpkg-deb &>/dev/null || fail "dpkg-deb not found. Run: sudo apt install dpkg-dev" +command -v curl &>/dev/null || fail "curl not found. Run: sudo apt install curl" + +# ── Prepare staging area ────────────────────────────────────────────────────── +info "Preparing build directory..." +PKG_STAGE="$BUILD_DIR/${PKG_NAME}_${PKG_VERSION}_${PKG_ARCH}" +rm -rf "$PKG_STAGE" +mkdir -p "$PKG_STAGE/DEBIAN" + +# ── Copy DEBIAN control files ───────────────────────────────────────────────── +info "Copying control files..." +cp "$DEBIAN_DIR/control" "$PKG_STAGE/DEBIAN/control" +cp "$DEBIAN_DIR/postinst" "$PKG_STAGE/DEBIAN/postinst" +cp "$DEBIAN_DIR/prerm" "$PKG_STAGE/DEBIAN/prerm" +cp "$DEBIAN_DIR/postrm" "$PKG_STAGE/DEBIAN/postrm" + +# Inject current version into control file +sed -i "s/^Version:.*/Version: $PKG_VERSION/" "$PKG_STAGE/DEBIAN/control" + +# Fix permissions — maintainer scripts must be executable +chmod 755 "$PKG_STAGE/DEBIAN/postinst" \ + "$PKG_STAGE/DEBIAN/prerm" \ + "$PKG_STAGE/DEBIAN/postrm" + +# ── Copy payload files ──────────────────────────────────────────────────────── +info "Copying payload files..." +cp -r "$DEBIAN_DIR/files/." "$PKG_STAGE/" + +# Copy the docker-compose.yml from repo root into the package +mkdir -p "$PKG_STAGE/opt/dupfinder" +cp "$REPO_ROOT/docker-compose.yml" "$PKG_STAGE/opt/dupfinder/docker-compose.yml" + +# Copy source as fallback build path +cp -r "$REPO_ROOT/app" "$PKG_STAGE/opt/dupfinder/source/" 2>/dev/null || true +cp -r "$REPO_ROOT/templates" "$PKG_STAGE/opt/dupfinder/source/" 2>/dev/null || true +cp "$REPO_ROOT/Dockerfile" "$PKG_STAGE/opt/dupfinder/source/" 2>/dev/null || true +cp "$REPO_ROOT/requirements.txt" "$PKG_STAGE/opt/dupfinder/source/" 2>/dev/null || true + +# ── Fix file permissions ────────────────────────────────────────────────────── +find "$PKG_STAGE" -type f -name "*.sh" -exec chmod 755 {} \; +chmod 755 "$PKG_STAGE/usr/local/bin/dupfinder" 2>/dev/null || true +# Directories must be 755, files 644 (except executables) +find "$PKG_STAGE" -type d -exec chmod 755 {} \; +find "$PKG_STAGE" -type f ! -name "*.sh" \ + ! -path "*/DEBIAN/*" \ + ! -name "dupfinder" \ + -exec chmod 644 {} \; + +ok "Staging area ready: $PKG_STAGE" + +# ── Build .deb ──────────────────────────────────────────────────────────────── +info "Building $DEB_FILE ..." +mkdir -p "$BUILD_DIR" +dpkg-deb --build --root-owner-group "$PKG_STAGE" "$BUILD_DIR/$DEB_FILE" + +DEB_SIZE=$(du -sh "$BUILD_DIR/$DEB_FILE" | cut -f1) +ok "Built: $BUILD_DIR/$DEB_FILE ($DEB_SIZE)" + +# ── Upload to Gitea ─────────────────────────────────────────────────────────── +if [[ "$NO_UPLOAD" == "true" ]]; then + echo "" + echo "Skipping upload (--no-upload). File is at:" + echo " $BUILD_DIR/$DEB_FILE" + exit 0 +fi + +info "Uploading to Gitea package registry..." +UPLOAD_URL="$GITEA_URL/api/packages/$GITEA_OWNER/debian/pool/$DISTRO/$COMPONENT" + +HTTP_STATUS=$(curl -s -o /tmp/gitea_upload_response.txt -w "%{http_code}" \ + -X PUT \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"$BUILD_DIR/$DEB_FILE" \ + "$UPLOAD_URL/$DEB_FILE") + +if [[ "$HTTP_STATUS" == "201" || "$HTTP_STATUS" == "200" ]]; then + ok "Uploaded successfully (HTTP $HTTP_STATUS)" +elif [[ "$HTTP_STATUS" == "409" ]]; then + echo " Package version $PKG_VERSION already exists in registry." + echo " Bump PKG_VERSION in this script to publish a new version." +else + echo " Upload failed (HTTP $HTTP_STATUS):" + cat /tmp/gitea_upload_response.txt + exit 1 +fi + +# ── Print install instructions ──────────────────────────────────────────────── +echo "" +echo "╔══════════════════════════════════════════════════════════════════╗" +echo "║ Package published! Install on any Ubuntu/Debian machine with: ║" +echo "╠══════════════════════════════════════════════════════════════════╣" +echo "║ ║" +echo "║ 1. Add the repo: ║" +echo "║ echo \"deb [trusted=yes] \\ ║" +echo "║ $GITEA_URL/api/packages/$GITEA_OWNER/debian \\ ║" +echo "║ $DISTRO $COMPONENT\" \\ ║" +echo "║ | sudo tee /etc/apt/sources.list.d/dupfinder.list ║" +echo "║ ║" +echo "║ 2. Install: ║" +echo "║ sudo apt update && sudo apt install dupfinder ║" +echo "║ ║" +echo "║ 3. Configure: ║" +echo "║ sudo dupfinder setup ║" +echo "║ ║" +echo "╚══════════════════════════════════════════════════════════════════╝" +echo "" diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..efe8fd3 --- /dev/null +++ b/debian/control @@ -0,0 +1,15 @@ +Package: dupfinder +Version: 1.0.0 +Architecture: amd64 +Maintainer: tocmo0nlord +Depends: docker.io | docker-ce, docker-compose-plugin | docker-compose +Recommends: nvidia-container-toolkit +Section: utils +Priority: optional +Description: Self-hosted duplicate photo and video finder + DupFinder scans a photo/video library using four detection methods: + exact hash (SHA-256), visual similarity (perceptual hash), EXIF + timestamp matching, and file-size/dimension matching. All decisions + are stored in SQLite — no files are ever moved or deleted. + GPU acceleration via NVIDIA CUDA is supported automatically. +Homepage: http://192.168.1.64:3000/tocmo0nlord/duplicate-finder diff --git a/debian/files/etc/systemd/system/dupfinder.service b/debian/files/etc/systemd/system/dupfinder.service new file mode 100644 index 0000000..3847dc6 --- /dev/null +++ b/debian/files/etc/systemd/system/dupfinder.service @@ -0,0 +1,33 @@ +[Unit] +Description=DupFinder Duplicate Photo Scanner +Documentation=http://192.168.1.64:3000/tocmo0nlord/duplicate-finder +After=docker.service network-online.target +Requires=docker.service +Wants=network-online.target + +[Service] +Type=simple +Restart=on-failure +RestartSec=10 +EnvironmentFile=-/etc/dupfinder.conf +WorkingDirectory=/opt/dupfinder + +ExecStart=/usr/bin/docker compose \ + -f /opt/dupfinder/docker-compose.yml \ + -f /opt/dupfinder/docker-compose.override.yml \ + up --no-build --remove-orphans + +ExecStop=/usr/bin/docker compose \ + -f /opt/dupfinder/docker-compose.yml \ + -f /opt/dupfinder/docker-compose.override.yml \ + down + +# Don't start if override hasn't been created yet (setup not run) +ExecStartPre=/bin/test -f /opt/dupfinder/docker-compose.override.yml + +StandardOutput=journal +StandardError=journal +SyslogIdentifier=dupfinder + +[Install] +WantedBy=multi-user.target diff --git a/debian/files/opt/dupfinder/dupfinder-setup.sh b/debian/files/opt/dupfinder/dupfinder-setup.sh new file mode 100644 index 0000000..f7e758d --- /dev/null +++ b/debian/files/opt/dupfinder/dupfinder-setup.sh @@ -0,0 +1,167 @@ +#!/bin/bash +# DupFinder first-time setup — configure paths, pull image, write override +set -e + +CONF_FILE="/etc/dupfinder.conf" +COMPOSE_DIR="/opt/dupfinder" +OVERRIDE_YML="$COMPOSE_DIR/docker-compose.override.yml" +IMAGE_NAME="tocmo0nlord/dupfinder:latest" +DATA_DIR="/var/lib/dupfinder/data" +APP_PORT=8765 + +RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${CYAN}==> $*${NC}"; } +ok() { echo -e "${GREEN} OK $*${NC}"; } +err() { echo -e "${RED} !! $*${NC}"; } + +# ── Root check ──────────────────────────────────────────────────────────────── +if [[ $EUID -ne 0 ]]; then + err "Please run as root: sudo dupfinder setup" + exit 1 +fi + +echo "" +echo " ╔══════════════════════════════════════╗" +echo " ║ DupFinder Setup ║" +echo " ╚══════════════════════════════════════╝" +echo "" + +# ── Load existing config as defaults ───────────────────────────────────────── +[[ -f "$CONF_FILE" ]] && source "$CONF_FILE" +: "${PHOTOS_PATH:=/mnt/photos}" +: "${DATA_PATH:=$DATA_DIR}" +: "${APP_PORT:=8765}" + +# ── Check Docker ────────────────────────────────────────────────────────────── +info "Checking Docker..." +if ! command -v docker &>/dev/null; then + err "Docker is not installed." + echo " Install with: curl -fsSL https://get.docker.com | sh" + exit 1 +fi +if ! docker info &>/dev/null; then + err "Docker daemon is not running." + echo " Start with: sudo systemctl start docker" + exit 1 +fi +ok "Docker is running" + +# ── Check docker compose ────────────────────────────────────────────────────── +if ! docker compose version &>/dev/null; then + err "docker compose (V2 plugin) not found. Update Docker or install docker-compose-plugin." + exit 1 +fi +ok "docker compose V2 available" + +# ── Check NVIDIA GPU ────────────────────────────────────────────────────────── +info "Checking GPU..." +if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null; then + GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1) + ok "NVIDIA GPU detected: $GPU_NAME" + GPU_AVAILABLE=true +else + echo " No NVIDIA GPU detected — will use CPU for perceptual hashing" + GPU_AVAILABLE=false +fi + +# ── Photos path ─────────────────────────────────────────────────────────────── +echo "" +info "Photos library path (mounted read-only):" +echo " Current: $PHOTOS_PATH" +read -rp " Path [Enter to keep]: " INPUT +INPUT="${INPUT%\"}" ; INPUT="${INPUT#\"}" # strip quotes +[[ -n "$INPUT" ]] && PHOTOS_PATH="$INPUT" + +if [[ ! -d "$PHOTOS_PATH" ]]; then + err "Path not found: $PHOTOS_PATH" + echo " Create it or mount your drive first, then re-run setup." + exit 1 +fi +ok "Photos: $PHOTOS_PATH" + +# ── Data path ───────────────────────────────────────────────────────────────── +echo "" +info "Database storage path:" +echo " Current: $DATA_PATH" +read -rp " Path [Enter to keep]: " INPUT +INPUT="${INPUT%\"}" ; INPUT="${INPUT#\"}" +[[ -n "$INPUT" ]] && DATA_PATH="$INPUT" +mkdir -p "$DATA_PATH" +ok "Data: $DATA_PATH" + +# ── Port ────────────────────────────────────────────────────────────────────── +echo "" +read -rp " Web port [$APP_PORT]: " INPUT +[[ -n "$INPUT" ]] && APP_PORT="$INPUT" +ok "Port: $APP_PORT" + +# ── Pull Docker image ───────────────────────────────────────────────────────── +echo "" +info "Pulling Docker image ($IMAGE_NAME)..." +if docker pull "$IMAGE_NAME"; then + ok "Image pulled" +else + echo "" + echo " Could not pull from registry. Trying to build from source..." + if [[ -f "$COMPOSE_DIR/source/Dockerfile" ]]; then + docker build -t "$IMAGE_NAME" "$COMPOSE_DIR/source" + ok "Image built from source" + else + err "Neither pull nor local build succeeded." + echo " Make sure you have internet access or the source files are present." + exit 1 + fi +fi + +# ── Write config + override ─────────────────────────────────────────────────── +info "Writing configuration..." + +cat > "$CONF_FILE" < "$OVERRIDE_YML" <> "$OVERRIDE_YML" </dev/null || \ + echo "Open in browser: http://localhost:$APP_PORT" + ;; + update) + _require_conf + echo "Pulling latest image..." + docker pull tocmo0nlord/dupfinder:latest + sudo systemctl restart dupfinder.service + echo "Updated and restarted." + ;; + uninstall) + echo "To fully remove DupFinder: sudo apt remove dupfinder" + echo "To also remove data: sudo apt purge dupfinder" + ;; + help|--help|-h|*) + echo "Usage: dupfinder " + echo "" + echo "Commands:" + echo " setup Configure photos path, data path, pull image" + echo " start Start the service" + echo " stop Stop the service" + echo " restart Restart the service" + echo " status Show systemd service status" + echo " logs Tail container logs" + echo " open Open in browser" + echo " update Pull latest image and restart" + echo " uninstall Show removal instructions" + ;; +esac diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..980ecb9 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +# Create data directory with correct permissions +mkdir -p /var/lib/dupfinder/data +chmod 755 /var/lib/dupfinder + +# Reload systemd and enable service (don't start yet — needs user config first) +systemctl daemon-reload +systemctl enable dupfinder.service 2>/dev/null || true + +echo "" +echo "╔══════════════════════════════════════════════════╗" +echo "║ DupFinder installed successfully! ║" +echo "╠══════════════════════════════════════════════════╣" +echo "║ ║" +echo "║ Run setup to configure your photos path: ║" +echo "║ ║" +echo "║ sudo dupfinder setup ║" +echo "║ ║" +echo "║ After setup, manage with: ║" +echo "║ sudo systemctl start dupfinder ║" +echo "║ sudo systemctl stop dupfinder ║" +echo "║ dupfinder status ║" +echo "║ ║" +echo "╚══════════════════════════════════════════════════╝" +echo "" diff --git a/debian/postrm b/debian/postrm new file mode 100644 index 0000000..8d553bd --- /dev/null +++ b/debian/postrm @@ -0,0 +1,13 @@ +#!/bin/bash +set -e +case "$1" in + purge) + # Remove data only on purge (not regular remove) + rm -rf /var/lib/dupfinder + rm -f /etc/dupfinder.conf + systemctl daemon-reload 2>/dev/null || true + ;; + remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + systemctl daemon-reload 2>/dev/null || true + ;; +esac diff --git a/debian/prerm b/debian/prerm new file mode 100644 index 0000000..9ea060b --- /dev/null +++ b/debian/prerm @@ -0,0 +1,5 @@ +#!/bin/bash +set -e +# Stop and disable service before removal +systemctl stop dupfinder.service 2>/dev/null || true +systemctl disable dupfinder.service 2>/dev/null || true