Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Version control
.git
.gitignore
.github

# Python bytecode & caches
**/__pycache__
**/*.pyc
**/*.pyo
*.egg-info
.mypy_cache
.ruff_cache

# Virtual environments (host-side; the image builds its own at /venv)
venv
.venv

# IDE / editor
.vscode
.cursor
.idea
*.swp
*.swo

# Docker files (not needed inside the image)
docker-compose.yml
Dockerfile
.dockerignore

# Documentation (not needed at runtime)
docs

# Runtime artifacts that should not leak into the build context
tmp
*.torrent
*.log

# OS junk
Thumbs.db
.DS_Store

# Tests / CI
tests
.bandit.yaml
bandit.yaml
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Ensure shell scripts always have Unix line endings (LF), even on Windows.
# Docker exec fails with "no such file or directory" if CRLF leaks into shebangs.
*.sh text eol=lf
docker-entrypoint.sh text eol=lf
78 changes: 58 additions & 20 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM python:3.12

# Update the package list and install system dependencies including mono
# ── System dependencies ──────────────────────────────────────────────
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git \
Expand All @@ -11,19 +11,23 @@ RUN apt-get update && \
rustc \
nano \
ca-certificates \
curl && \
curl \
gosu && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
update-ca-certificates

# Set up a virtual environment to isolate our Python dependencies
# ── Python environment ──────────────────────────────────────────────
# Ensure Python output is sent straight to the container logs (no buffering)
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

RUN python -m venv /venv
ENV PATH="/venv/bin:$PATH"

# Install wheel, requests (for DVD MediaInfo download), and other Python dependencies
RUN pip install --upgrade pip==25.3 wheel==0.45.1 requests==2.32.5
RUN pip install --no-cache-dir --upgrade pip==25.3 wheel==0.45.1 requests==2.32.5

# Set the working directory in the container
# ── Application setup ────────────────────────────────────────────────
WORKDIR /Upload-Assistant

# Copy DVD MediaInfo download script and run it
Expand All @@ -33,31 +37,65 @@ RUN python3 bin/get_dvd_mediainfo_docker.py

# Copy the Python requirements file and install Python dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application
COPY . .

# Preserve the built-in data/ directory outside the mount-point so that
# volume mounts over /Upload-Assistant/data/ don't hide critical files
# (__init__.py, version.py, example-config.py, templates/).
# At runtime the app restores any missing files from this copy.
RUN rm -rf /Upload-Assistant/defaults \
&& mkdir -p /Upload-Assistant/defaults \
&& cp -a data /Upload-Assistant/defaults/ \
&& find /Upload-Assistant/defaults/ -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true

# Download only the required mkbrr binary (requires full repo for src imports)
RUN python3 -c "from bin.get_mkbrr import MkbrrBinaryManager; MkbrrBinaryManager.download_mkbrr_for_docker()"

# Ensure binaries are executable
RUN find bin/mkbrr -name "mkbrr" -print0 | xargs -0 chmod +x

# Download bdinfo binary for the container architecture using the docker helper
RUN python3 bin/get_bdinfo_docker.py

# Ensure bdinfo binaries are executable
RUN find bin/bdinfo -name "bdinfo" -print0 | xargs -0 chmod +x
# Ensure downloaded binaries are executable
RUN find bin/mkbrr -name "mkbrr" -print0 | xargs -0 chmod +x && \
find bin/bdinfo -name "bdinfo" -print0 | xargs -0 chmod +x

# Enable non-root access while still letting Upload-Assistant tighten permissions at runtime
RUN chown -R 1000:1000 /Upload-Assistant/bin/mkbrr
RUN chown -R 1000:1000 /Upload-Assistant/bin/MI
RUN chown -R 1000:1000 /Upload-Assistant/bin/bdinfo
# ── Permissions ──────────────────────────────────────────────────────
# Give UID 1000 ownership (runtime binary updates need chmod) and let
# any other UID (e.g. Unraid 99:100) read/execute.
RUN chown -R 1000:1000 /Upload-Assistant/bin/mkbrr \
&& chown -R 1000:1000 /Upload-Assistant/bin/MI \
&& chown -R 1000:1000 /Upload-Assistant/bin/bdinfo \
&& chmod -R o+rX /Upload-Assistant/bin/mkbrr \
&& chmod -R o+rX /Upload-Assistant/bin/MI \
&& chmod -R o+rX /Upload-Assistant/bin/bdinfo

# Create tmp directory with appropriate permissions
RUN mkdir -p /Upload-Assistant/tmp && chmod 777 /Upload-Assistant/tmp
# Create tmp directory; world-writable so any UID can use it
RUN mkdir -p /Upload-Assistant/tmp && chmod 1777 /Upload-Assistant/tmp
ENV TMPDIR=/Upload-Assistant/tmp

# Set the entry point for the container
ENTRYPOINT ["python", "/Upload-Assistant/upload.py"]
# ── Runtime metadata ─────────────────────────────────────────────────
# Document the WebUI port (informational only; does not publish the port)
EXPOSE 5000

# Let Docker send SIGTERM for graceful shutdown (Python handles it in upload.py)
STOPSIGNAL SIGTERM

# Health check for WebUI mode — ignored when running CLI
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -sf http://localhost:5000/api/health || exit 1

# ── Entrypoint ───────────────────────────────────────────────────────
# The entrypoint script handles directory permissions and optional
# privilege-drop via PUID/PGID environment variables.
# Pass arguments via CMD or `docker run ... <args>`.
# WebUI : docker run ... image --webui 0.0.0.0:5000
# CLI : docker run ... image /data/content --trackers BHD
COPY docker-entrypoint.sh /usr/local/bin/
RUN sed -i 's/\r$//' /usr/local/bin/docker-entrypoint.sh \
&& chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

# Default: show help when no arguments are provided
CMD ["-h"]
110 changes: 89 additions & 21 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,99 @@
# Upload Assistant — Docker Compose
# Docs: docs/docker-wiki-full.md & docs/docker-gui-wiki-full.md
#
# The entrypoint handles directory permissions and privilege-drop.
# Pass arguments via `command:`.
#
# ── WebUI service ────────────────────────────────────────────────────
services:
upload-assistant-cli:
upload-assistant:
image: ghcr.io/audionut/upload-assistant:latest
container_name: UA
container_name: upload-assistant
restart: unless-stopped

networks:
- yournetwork #change to the network with ur torrent instance
ports:
# Change left side to your specific port. Use 0.0.0.0:5000:5000 to expose to ANY device.
# 127.0.0.1 means only accessible from the host machine. If you change to 0.0.0.0:5000:5000 it will be accessible from any device on the network
# including devices outside your network if your router/reverse proxy allows it.
- yournetwork # change to the network with your torrent client

ports:
# Change host-side port/binding as needed.
# 127.0.0.1 → accessible only from the host machine
# 0.0.0.0 → accessible from any device on the network
- "127.0.0.1:5000:5000"

# ── Pass --webui as the CMD (appended to the ENTRYPOINT) ────────
command: ["--webui", "0.0.0.0:5000"]

environment:
# Required if not using paths from terminal: allowlisted roots the Web UI is allowed to browse/execute within
# Optional: run the app as a specific user/group (recommended).
# The entrypoint starts as root, fixes directory ownership, then
# drops to this UID/GID before running the app.
# If omitted the app runs as root inside the container.
- PUID=1000
- PGID=1000

# Required in Docker: container-side roots the WebUI is allowed to browse.
# Docker typically runs with `--webui` only (no paths), so without this the
# app would use a dummy path and the file browser would not work.
# Must match the right side (container path) of each `volumes:` entry.
# Enables granular access: mount a volume but only expose specific
# subpaths to the WebUI (e.g. omit /torrent_storage_dir).
- UA_BROWSE_ROOTS=/data/torrents,/Upload-Assistant/tmp
# Optional: set secret key as key, or use file variant to read from a file. (minimum 32 bytes) permissions handling will apply.
# - ENV_SESSION_SECRET="SESSION_SECRET"
# - ENV_SESSION_SECRET_FILE="SESSION_SECRET_FILE"
# Optional: only needed if you serve the UI from a different origin/domain.

# Optional: explicit Docker detection (auto-detected in most cases).
# Accepts: 1, true, yes. Also recognised as RUNNING_IN_CONTAINER.
# - IN_DOCKER=1

# Optional: stable session secret so encrypted WebUI credentials
# survive container recreates. Provide either the raw value or a
# path to a file (minimum 32 bytes).
# - SESSION_SECRET=your-secret-here
# - SESSION_SECRET_FILE=/Upload-Assistant/data/session_secret
#
# NOTE: If you mount a volume to SESSION_SECRET_FILE and the host
# path does not already exist as a *file*, Docker will create it as
# a directory. The entrypoint fixes ownership of /Upload-Assistant/
# session_secret when PUID/PGID are set, so the app can create the
# file inside. The recommended approach is to mount the webui-auth
# volume (see below) and let the app auto-generate the secret there.

# Optional: only needed if you serve the UI from a different origin.
# - UA_WEBUI_CORS_ORIGINS=https://your-ui-host
entrypoint: /bin/bash
command: -c "source /venv/bin/activate && exec python /Upload-Assistant/upload.py --webui 0.0.0.0:5000"

# Optional: override the XDG config directory inside the container.
# Default is /root/.config/upload-assistant (stores session_secret,
# webui_auth.json). Only change if you mount a different path.
# - XDG_CONFIG_HOME=/custom/config/path

volumes:
- /path/to/torrents/:/data/torrents/:rw #map this to qbit download location, map exactly as qbittorent template on both sides.
- /mnt/user/appdata/Upload-Assistant/data/config.py:/Upload-Assistant/data/config.py:rw # Optional: will be created automatically if missing
- /mnt/user/appdata/qBittorrent/data/BT_backup/:/torrent_storage_dir:rw #map this to your qbittorrent bt_backup
- /mnt/user/appdata/Upload-Assistant/tmp/:/Upload-Assistant/tmp:rw #map this to your /tmp folder.
- /mnt/user/appdata/Upload-Assistant/webui-auth:/root/.config/upload-assistant:rw # persist web UI session auth config
# Map your torrent download directory (match qBittorrent paths).
- /path/to/torrents:/data/torrents:rw

# Application data — config.py is created automatically from
# example-config.py on first WebUI start. The directory does NOT
# need to exist on the host; Docker + the entrypoint create it
# with the correct ownership.
- /path/to/appdata/Upload-Assistant/data:/Upload-Assistant/data:rw

# qBittorrent BT_backup for torrent reuse.
- /path/to/qBittorrent/BT_backup:/torrent_storage_dir:rw

# Temp directory for screenshots / processing.
- /path/to/appdata/Upload-Assistant/tmp:/Upload-Assistant/tmp:rw

# Persist WebUI session auth config (webui_auth.json, session_secret).
# This maps to the XDG config dir used inside the container.
- /path/to/appdata/Upload-Assistant/webui-auth:/root/.config/upload-assistant:rw

# Give the app time to finish any in-flight work on shutdown.
stop_grace_period: 15s

healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:5000/api/health"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3

networks:
"yournetwork": #change this to your network
external: true
yournetwork: # change to your network
external: true
70 changes: 70 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/bin/sh
set -e

# ── Docker entrypoint ─────────────────────────────────────────────────
# Handles directory ownership so that fresh volume mounts (created as
# root by Docker) are writable by the runtime user.
#
# Supports two modes:
# 1. PUID/PGID env vars (recommended) — container starts as root,
# fixes permissions, then drops to the requested UID/GID.
# 2. No PUID/PGID — runs as whatever user Docker started (root or
# the UID from `user:` in compose / --user on CLI).
# ──────────────────────────────────────────────────────────────────────

TARGET_UID="${PUID:-}"
TARGET_GID="${PGID:-}"

# ── Fix directory ownership (only possible when running as root) ──────
if [ "$(id -u)" = "0" ]; then
# Directories the app needs write access to
# - data, tmp: config, temp files
# - session_secret: when SESSION_SECRET_FILE points to a path that Docker
# created as a directory (host path didn't exist), the app creates a
# session_secret file inside it; the runtime user must be able to write
# - /root/.config/upload-assistant: webui-auth mount; when PUID is set,
# the runtime user must traverse /root and write there
for dir in /Upload-Assistant/data /Upload-Assistant/tmp /Upload-Assistant/session_secret /root/.config/upload-assistant; do
# If the path already exists as a non-directory (e.g. a file bind-mount),
# fix its ownership but don't try mkdir -p (which would fail under set -e).
if [ -e "$dir" ] && [ ! -d "$dir" ]; then
if [ -n "$TARGET_UID" ]; then
chown "$TARGET_UID:${TARGET_GID:-$TARGET_UID}" "$dir" 2>/dev/null || true
fi
continue
fi
mkdir -p "$dir"
if [ -n "$TARGET_UID" ]; then
# Recursively fix ownership so that user-placed files (e.g. config.py
# copied onto the host while the container was stopped) are owned by
# the runtime user.
chown -R "$TARGET_UID:${TARGET_GID:-$TARGET_UID}" "$dir" 2>/dev/null || true
fi
# Ensure sane permissions: directories traversable, files readable/writable
# by the owner. Bind mounts from Unraid / NAS hosts can arrive with any
# mode bits; normalise them so the app can always read and write.
find "$dir" -type d ! -perm -u=rwx -exec chmod u+rwx {} + 2>/dev/null || true
find "$dir" -type f ! -perm -u=rw -exec chmod u+rw {} + 2>/dev/null || true
done

# When dropping to non-root, the runtime user must traverse /root to reach
# /root/.config/upload-assistant (webui-auth mount). Make /root traversable.
if [ -n "$TARGET_UID" ] && [ "$TARGET_UID" != "0" ]; then
chmod 711 /root 2>/dev/null || true
fi

# Drop privileges if PUID was set
if [ -n "$TARGET_UID" ] && [ "$TARGET_UID" != "0" ]; then
# Ensure XDG_CONFIG_HOME is set so the app resolves the config
# directory reliably after gosu drops privileges. When the target
# UID has no /etc/passwd entry (common in containers), Path.home()
# returns "/" and the mounted /root/.config/upload-assistant would
# never be found.
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-/root/.config}"

exec gosu "$TARGET_UID:${TARGET_GID:-$TARGET_UID}" python /Upload-Assistant/upload.py "$@"
fi
fi

# Fallback: run as current user (root, or whatever `user:` specified)
exec python /Upload-Assistant/upload.py "$@"
Loading