Merge pull request 'feat(copyparty): add file-server deployment with SFTP/FTPS + security-notices updater' (#5) from claude/agitated-bose-f427e5 into main
Reviewed-on: #5
This commit is contained in:
@@ -23,6 +23,11 @@ id_ed25519_*
|
||||
# committed (the private key can MITM any client that trusts it).
|
||||
deployments/squid/ssl/
|
||||
|
||||
# copyparty generated config -- cfg/copyparty.conf holds the admin password and
|
||||
# cfg/ftps.pem the FTPS key; both are generated on the host at deploy time. The
|
||||
# copyparty.conf.example template stays tracked.
|
||||
deployments/copyparty/cfg/
|
||||
|
||||
# ── Backups ─────────────────────────────────────────────────────────────────
|
||||
*.tar.gz.age
|
||||
*-backup-*.tar.gz*
|
||||
|
||||
@@ -108,6 +108,7 @@ deployments/<name>/ # one folder per stack
|
||||
| [`headscale`](deployments/headscale/) | Self-hosted Tailscale control server, OIDC login. | pocket-id (OIDC) |
|
||||
| [`webfinger`](deployments/webfinger/) | Serves `/.well-known/webfinger` for OIDC discovery; redirects the rest. | pocket-id (issuer) |
|
||||
| [`squid`](deployments/squid/) | SSL-bump caching forward proxy — static-content cache + TLS interception via a local CA. **The exception: a forward proxy, not a Caddy/LE site.** | — |
|
||||
| [`copyparty`](deployments/copyparty/) | Portable file server — web UI/WebDAV behind Caddy, plus direct **SFTP** + **FTPS**. Ships a security-notices-aware updater. | — |
|
||||
| [`simplex`](deployments/simplex/) | SimpleX SMP + XFTP relay with Tor hidden services + encrypted backups. | globals/age-pubkey.txt |
|
||||
|
||||
## Conventions
|
||||
@@ -142,8 +143,8 @@ instance user-data, and the host configures itself on first boot.
|
||||
## Multi-OS notes
|
||||
|
||||
The host-provisioning scripts (`setup-host`, `harden-ssh`, `harden-jumphost`,
|
||||
`sshuser`) and the five Docker stacks (pocket-id, beszel, headscale, webfinger,
|
||||
squid) run on Alpine, Debian, and Alma. Distro differences live in
|
||||
`sshuser`) and the six Docker stacks (pocket-id, beszel, headscale, webfinger,
|
||||
squid, copyparty) run on Alpine, Debian, and Alma. Distro differences live in
|
||||
[`scripts/oslib.sh`](scripts/oslib.sh) — package manager (`apk`/`apt`/`dnf`),
|
||||
init system (OpenRC/systemd), sshd service name, the per-distro `sftp-server`
|
||||
path, hostname, boot hooks, and the sshguard log source/backend.
|
||||
|
||||
+10
-3
@@ -13,8 +13,9 @@
|
||||
#
|
||||
# It opens a Gum wizard (auto-installed) that lets you:
|
||||
# • Mode: deploy on THIS host, or build deploy.sh artifacts locally.
|
||||
# • Pick any deployment (pocket-id, beszel, headscale, webfinger, simplex)
|
||||
# or any generic script (harden-ssh, harden-jumphost, sshuser).
|
||||
# • Pick any deployment (pocket-id, beszel, headscale, webfinger, squid,
|
||||
# copyparty, simplex) or any generic script (harden-ssh, harden-jumphost,
|
||||
# sshuser, auto-update).
|
||||
# Shared defaults come from globals/ (see globals/README.md).
|
||||
#
|
||||
# Non-interactive: set SKIP_PROMPTS=1 plus the needed vars and pipe the menu
|
||||
@@ -62,7 +63,7 @@ fi
|
||||
. "$ROOT/scripts/lib.sh"
|
||||
load_globals
|
||||
|
||||
DEPLOYMENTS=(pocket-id beszel headscale webfinger squid simplex)
|
||||
DEPLOYMENTS=(pocket-id beszel headscale webfinger squid copyparty simplex)
|
||||
SCRIPTS=(setup-host harden-ssh harden-jumphost sshuser auto-update)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -113,6 +114,12 @@ ask_deployment_vars() {
|
||||
ask BIND_ADDR "Host IP to bind the proxy on (blank = 0.0.0.0)" optional
|
||||
ask CACHE_SIZE_MB "On-disk cache size in MB (blank = 5000)" optional
|
||||
ask CACHE_ONLY_LISTED "Cache ONLY listed domains? (1=yes, blank=boost mode)" optional ;;
|
||||
copyparty)
|
||||
ask COPYPARTY_DOMAIN "Public hostname for the web UI (e.g. files.example.com)"
|
||||
ask ACME_EMAIL "Let's Encrypt email"
|
||||
ask DATA_DIR "Host data folder shared as the root (blank = /srv/copyparty/data)" optional
|
||||
ask FTP_NAT "Public IP for passive FTPS via NAT (blank = none)" optional
|
||||
ask UPDATE_POLICY "Auto-update policy: latest | security | off (blank = latest)" optional ;;
|
||||
simplex)
|
||||
ask DOMAIN "Apex domain (creates smp.DOMAIN, xftp.DOMAIN)"
|
||||
ask ACME_EMAIL "Let's Encrypt email"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copy to .env and fill in. docker compose picks .env up automatically.
|
||||
|
||||
# --- Public hostname (web UI / WebDAV via Caddy) ----------------------------
|
||||
# Bare hostname (no scheme) where the copyparty web UI is reached.
|
||||
COPYPARTY_DOMAIN=files.example.com
|
||||
|
||||
# Email for Let's Encrypt registration / expiry notifications.
|
||||
ACME_EMAIL=admin@example.com
|
||||
|
||||
# --- Image -----------------------------------------------------------------
|
||||
# `ac` is the recommended edition (Pillow + FFmpeg thumbnails/transcode).
|
||||
# Use ghcr.io/9001/copyparty-ac to pull from GitHub instead of Docker Hub.
|
||||
COPYPARTY_IMAGE=copyparty/ac
|
||||
# Pinned version, e.g. 1.20.11. deploy.sh resolves "latest" to the newest
|
||||
# release on first deploy so the running version is always explicit; update.sh
|
||||
# bumps this tag when it updates.
|
||||
COPYPARTY_TAG=latest
|
||||
|
||||
# --- Data + identity --------------------------------------------------------
|
||||
# Host directory shared as the data root (mounted at /w). Created by deploy.sh
|
||||
# and chowned to PUID:PGID. Point this at a big disk / mount for real use.
|
||||
DATA_DIR=/srv/copyparty/data
|
||||
PUID=1000
|
||||
PGID=1000
|
||||
|
||||
# --- SFTP / FTPS (direct listeners, bypass Caddy) ---------------------------
|
||||
# Host interface to publish the direct listeners on (blank = 0.0.0.0 = all).
|
||||
# A published Docker port bypasses the host firewall, so pin this to a trusted
|
||||
# interface (e.g. a Tailscale IP) on multi-homed boxes.
|
||||
BIND_ADDR=
|
||||
SFTP_PORT=3922
|
||||
FTPS_PORT=3990
|
||||
# Passive-FTP TCP port range (hyphen form; published as-is in docker-compose).
|
||||
FTP_PASV_RANGE=12000-12099
|
||||
# Public IP for passive FTPS through NAT. Leave blank if clients reach the
|
||||
# server directly; set it to the server's external IP otherwise.
|
||||
FTP_NAT=
|
||||
|
||||
# --- Updates (scripts: update.sh) -------------------------------------------
|
||||
# How the scheduled updater behaves:
|
||||
# latest - update to the newest release whenever one exists (default)
|
||||
# security - update ONLY when the running version has a known advisory
|
||||
# off - never change the running version (check/notify only)
|
||||
UPDATE_POLICY=latest
|
||||
# Advisory feed used by `security` policy + `update.sh check`:
|
||||
# advisories-panic (critical only) | advisories (important) | advisories-all
|
||||
VC_FEED=advisories
|
||||
|
||||
# --- Image tags -------------------------------------------------------------
|
||||
CADDY_TAG=2-alpine
|
||||
@@ -0,0 +1,42 @@
|
||||
# Caddyfile for the copyparty stack.
|
||||
#
|
||||
# Auto-issues a Let's Encrypt cert for $COPYPARTY_DOMAIN and reverse-proxies the
|
||||
# web UI / WebDAV to copyparty on :3923. SFTP and FTPS do NOT pass through here
|
||||
# -- they are published straight from the copyparty container.
|
||||
|
||||
{
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
{$COPYPARTY_DOMAIN} {
|
||||
encode zstd gzip
|
||||
|
||||
reverse_proxy copyparty:3923 {
|
||||
# Real client IP: Caddy already appends the genuine remote address to
|
||||
# X-Forwarded-For (it is the internet edge); copyparty trusts it via
|
||||
# `xff-src` in copyparty.conf. X-Real-IP is set too for good measure.
|
||||
header_up X-Real-IP {http.request.remote.host}
|
||||
|
||||
# Stream uploads/downloads (no buffering) and allow long-lived large
|
||||
# transfers -- copyparty resumable uploads can run for a long time.
|
||||
# Caddy imposes no request-body size limit by default, so big uploads
|
||||
# stream straight through.
|
||||
flush_interval -1
|
||||
transport http {
|
||||
read_timeout 1h
|
||||
write_timeout 1h
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
-Server
|
||||
}
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format console
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
# copyparty
|
||||
|
||||
[copyparty](https://github.com/9001/copyparty) — a portable file server with
|
||||
resumable uploads, dedup, WebDAV, **SFTP**, **FTPS**, media indexing and
|
||||
thumbnails — behind Caddy with automatic Let's Encrypt TLS. Ships with a
|
||||
**security-notices-aware updater** (`update.sh`).
|
||||
|
||||
The web UI / WebDAV is fronted by Caddy on 443; **SFTP and FTPS are served
|
||||
directly by copyparty** (Caddy is HTTP-only and not in that path).
|
||||
|
||||
## Required `.env` values
|
||||
|
||||
| Variable | Notes |
|
||||
|----------|-------|
|
||||
| `COPYPARTY_DOMAIN` | Public hostname for the web UI (e.g. `files.example.com`). |
|
||||
| `ACME_EMAIL` | Let's Encrypt registration email. |
|
||||
| `DATA_DIR` | Host folder shared as the data root (mounted at `/w`). Point at a data disk. |
|
||||
| `COPYPARTY_IMAGE` / `COPYPARTY_TAG` | Image edition (`copyparty/ac`) + pinned version. |
|
||||
| `SFTP_PORT` / `FTPS_PORT` / `FTP_PASV_RANGE` | Direct-listener ports (`3922` / `3990` / `12000-12099`). |
|
||||
| `FTP_NAT` | This host's public IP, for passive FTPS through NAT (blank if direct). |
|
||||
| `UPDATE_POLICY` | `latest` (default) / `security` / `off` — see [Updates](#updates--security-notices). |
|
||||
|
||||
See [`.env.example`](.env.example) for the rest. Accounts and shares live in
|
||||
[`copyparty.conf.example`](copyparty.conf.example) (deploy.sh generates the real
|
||||
`cfg/copyparty.conf` with a random admin password on first run).
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
./automations.sh # Deploy on this host → deploy: copyparty
|
||||
```
|
||||
|
||||
Or build + run the self-contained artifact:
|
||||
|
||||
```bash
|
||||
./build.sh
|
||||
scp deploy.sh root@host:
|
||||
ssh root@host 'bash deploy.sh'
|
||||
# non-interactive:
|
||||
# COPYPARTY_DOMAIN=files.example.com ACME_EMAIL=me@example.com SKIP_PROMPTS=1 bash deploy.sh
|
||||
```
|
||||
|
||||
Unattended provisioning: [`cloud-init.yml`](cloud-init.yml). On first run deploy.sh
|
||||
pins `COPYPARTY_TAG` to the newest release, generates `cfg/copyparty.conf` (random
|
||||
admin password, printed once) and a self-signed `cfg/ftps.pem`, opens the ports,
|
||||
brings the stack up, and schedules `update.sh`.
|
||||
|
||||
## Accounts & shares
|
||||
|
||||
The default config is **private**: one `admin` account with full access to the
|
||||
whole data root, nothing public. Edit `cfg/copyparty.conf` (commented examples
|
||||
included) to add users or open read-only / anonymous-upload shares, then hot-reload:
|
||||
|
||||
```bash
|
||||
docker compose exec copyparty kill -s USR1 1
|
||||
```
|
||||
|
||||
## Real client IP (behind Caddy)
|
||||
|
||||
For the web UI, copyparty must see the genuine client IP (for its logs and abuse
|
||||
bans), which means **both** sides are configured:
|
||||
|
||||
- **Caddy** is the internet edge, so it appends the real address to
|
||||
`X-Forwarded-For` and also sets `X-Real-IP` (see [`Caddyfile`](Caddyfile)).
|
||||
- **copyparty** trusts that upstream via `xff-src: lan` in
|
||||
[`copyparty.conf.example`](copyparty.conf.example) — without it copyparty
|
||||
ignores the header and logs/bans the Docker-network address.
|
||||
|
||||
`lan` (trust private ranges) is safe here because copyparty's HTTP port is never
|
||||
published — only Caddy can reach it. If copyparty prints an `xff-src` warning at
|
||||
startup, narrow it to the exact subnet it names. SFTP/FTPS are direct TCP, so
|
||||
they already see the real client IP (no XFF involved).
|
||||
|
||||
## SFTP & FTPS
|
||||
|
||||
Both authenticate against the same `[accounts]` as the web UI.
|
||||
|
||||
```bash
|
||||
sftp -P 3922 admin@files.example.com # SFTP (password auth enabled)
|
||||
lftp -u admin -e 'set ftp:ssl-force true; set ssl:verify-certificate no' \
|
||||
ftp://files.example.com:3990 # explicit-TLS FTPS
|
||||
```
|
||||
|
||||
- **SFTP host key** is generated on first start and persisted under `/cfg`, so
|
||||
clients don't get "host key changed" warnings across restarts.
|
||||
- **FTPS cert** defaults to the self-signed `cfg/ftps.pem` (clients must accept
|
||||
it). To serve a publicly-trusted cert, reuse Caddy's Let's Encrypt cert: mount
|
||||
the `caddy-data` volume read-only into the copyparty service and point
|
||||
copyparty at it, e.g.
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml, copyparty service:
|
||||
volumes:
|
||||
- caddy-data:/caddy:ro
|
||||
```
|
||||
```ini
|
||||
# cfg/copyparty.conf [global]:
|
||||
cert: /caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/files.example.com/files.example.com.crt
|
||||
certkey: /caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/files.example.com/files.example.com.key
|
||||
```
|
||||
(copyparty re-reads the cert on `kill -s USR1`; refresh after a renewal.)
|
||||
- **Passive FTPS** needs the `FTP_PASV_RANGE` ports published (they are) **and**
|
||||
`FTP_NAT` set to the server's public IP when clients connect through NAT.
|
||||
- A published Docker port bypasses the host `INPUT` firewall, so set `BIND_ADDR`
|
||||
to pin the listeners to a trusted interface on multi-homed hosts.
|
||||
|
||||
## Updates & security notices
|
||||
|
||||
copyparty doesn't self-update, but it publishes a machine-readable
|
||||
**security-advisories** feed (the same one its built-in `--vc-url` check uses).
|
||||
This deployment uses it on **two** levels:
|
||||
|
||||
1. **In-app** — `vc-url` + `vc-age` in `cfg/copyparty.conf` make copyparty itself
|
||||
check whether the running version has a known advisory and warn in the log /
|
||||
control panel. (`vc-exit`, opt-in, makes it shut down if vulnerable.)
|
||||
2. **[`update.sh`](update.sh)** — reads the same feed to *act*, pinning the new
|
||||
version in `.env` and recreating the container (with health-checked rollback).
|
||||
|
||||
```bash
|
||||
./update.sh check # report current vs latest + any advisory; change nothing
|
||||
./update.sh update # update now (TARGET_VERSION=1.20.11 to pin a version)
|
||||
./update.sh install # (re)schedule the daily run / uninstall to stop
|
||||
```
|
||||
|
||||
`UPDATE_POLICY` (in `.env` or `/etc/copyparty-update.conf`) controls the
|
||||
scheduled `run`:
|
||||
|
||||
| Policy | Behaviour |
|
||||
|--------|-----------|
|
||||
| `latest` *(default)* | Update to the newest release whenever one exists. |
|
||||
| `security` | Update **only** when the running version has a known advisory, to the patched release named in the feed. |
|
||||
| `off` | Never change the running version (check/notify only). |
|
||||
|
||||
`VC_FEED` selects the feed (`advisories-panic` / `advisories` / `advisories-all`).
|
||||
Update results reuse the ntfy config at `/etc/ssh-notify.conf` (same as the SSH
|
||||
login notifier / host auto-update), when present.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `docker-compose.yml` | caddy + copyparty; publishes SFTP/FTPS/passive ports. |
|
||||
| `Caddyfile` | TLS + reverse proxy for the web UI/WebDAV (real-IP headers). |
|
||||
| `copyparty.conf.example` | Config template (globals, security check, SFTP/FTPS, accounts, volumes). |
|
||||
| `update.sh` | Security-notices-aware container updater. |
|
||||
| `.env.example` | Stack tunables. |
|
||||
| `deploy.sh` / `build.sh` | Self-contained installer + archive embedder. |
|
||||
| `cloud-init.yml` | Fresh-VM bootstrap (harden SSH, then deploy). |
|
||||
|
||||
## Notes
|
||||
|
||||
- No Anubis PoW gate — it would break WebDAV / API / upload clients.
|
||||
- The web UI/WebDAV go through Caddy; SFTP/FTPS bypass it by design.
|
||||
- DNS for `COPYPARTY_DOMAIN` must resolve to the host and 80/443 be reachable
|
||||
before deploy for the LE cert to issue.
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# build.sh -- (re)embed the loose deployment files into deploy.sh as a base64
|
||||
# tar.gz payload after __ARCHIVE_BELOW__. Idempotent: strips any existing
|
||||
# payload first.
|
||||
#
|
||||
# Run this after editing ANY embedded file below, then re-stage deploy.sh --
|
||||
# the deployed stack uses the EMBEDDED copies, not the loose files.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
SCRIPT="$DIR/deploy.sh"
|
||||
MARKER="__ARCHIVE_BELOW__"
|
||||
|
||||
# README, cloud-init and the build/deploy scripts are NOT embedded. The generated
|
||||
# cfg/ (copyparty.conf + ftps.pem) is produced on the host at deploy time.
|
||||
FILES=(
|
||||
docker-compose.yml
|
||||
Caddyfile
|
||||
copyparty.conf.example
|
||||
.env.example
|
||||
update.sh
|
||||
)
|
||||
|
||||
[[ -f "$SCRIPT" ]] || { echo "deploy.sh not found at $SCRIPT" >&2; exit 1; }
|
||||
for f in "${FILES[@]}"; do
|
||||
[[ -f "$DIR/$f" ]] || { echo "Missing $DIR/$f" >&2; exit 1; }
|
||||
done
|
||||
|
||||
PAYLOAD=$(tar -czf - -C "$DIR" "${FILES[@]}" | base64)
|
||||
|
||||
TMP=$(mktemp)
|
||||
trap 'rm -f "$TMP"' EXIT
|
||||
|
||||
sed "/^${MARKER}\$/,\$d" "$SCRIPT" > "$TMP"
|
||||
{
|
||||
echo "$MARKER"
|
||||
echo "$PAYLOAD"
|
||||
} >> "$TMP"
|
||||
|
||||
mv "$TMP" "$SCRIPT"
|
||||
chmod +x "$SCRIPT"
|
||||
trap - EXIT
|
||||
|
||||
size=$(wc -c < "$SCRIPT")
|
||||
echo "Built $SCRIPT (${size} bytes)"
|
||||
@@ -0,0 +1,41 @@
|
||||
#cloud-config
|
||||
#
|
||||
# copyparty (file server) — harden SSH, then deploy, on a fresh host.
|
||||
#
|
||||
# Fill in REPO_URL and the values in the runcmd block, then paste this as the
|
||||
# instance user-data. DNS for COPYPARTY_DOMAIN must point at this host and ports
|
||||
# 80/443 must be reachable before boot, or the Let's Encrypt cert request fails.
|
||||
# SFTP/FTPS (3922/3990 + the passive range) are opened too.
|
||||
|
||||
packages:
|
||||
- git
|
||||
|
||||
runcmd:
|
||||
- hostnamectl set-hostname copyparty || true
|
||||
- |
|
||||
set -e
|
||||
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git
|
||||
REPO_BRANCH=main
|
||||
HARDEN_SSH=1 # harden SSH on this fresh VM (set 0 to skip)
|
||||
SSH_PORT=22
|
||||
ALLOWED_IP= # optional: whitelist your client IP in sshguard
|
||||
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" /opt/automations
|
||||
cd /opt/automations
|
||||
|
||||
# Harden SSH first (PQ KEX, key-only auth, sshguard + deny-by-default
|
||||
# firewall). The firewall it installs is what deploy.sh registers the
|
||||
# copyparty ports with.
|
||||
if [ "$HARDEN_SSH" = 1 ]; then
|
||||
SSH_PORT="$SSH_PORT" ALLOWED_IP="$ALLOWED_IP" SKIP_PROMPTS=1 FORCE=1 \
|
||||
bash scripts/harden-ssh.sh
|
||||
fi
|
||||
|
||||
# Deploy copyparty. DATA_DIR is the shared folder (point it at a data disk);
|
||||
# FTP_NAT should be this host's public IP if you want passive FTPS via NAT.
|
||||
COPYPARTY_DOMAIN=files.example.com \
|
||||
ACME_EMAIL=admin@example.com \
|
||||
DATA_DIR=/srv/copyparty/data \
|
||||
UPDATE_POLICY=latest \
|
||||
FTP_NAT= \
|
||||
SKIP_PROMPTS=1 \
|
||||
bash deployments/copyparty/deploy.sh
|
||||
@@ -0,0 +1,62 @@
|
||||
# copyparty configuration (TEMPLATE).
|
||||
#
|
||||
# deploy.sh copies this to cfg/copyparty.conf on first deploy, replacing
|
||||
# __ADMIN_PW__ with a generated password (and uncommenting ftp-nat if FTP_NAT is
|
||||
# set). The image auto-includes every *.conf in /cfg (its baked-in config ends
|
||||
# with `% /cfg`), so this file is loaded automatically -- no -c flag needed.
|
||||
#
|
||||
# Reload after editing, no restart: docker compose exec copyparty kill -s USR1 1
|
||||
# Permission letters: r=read w=write/upload m=move/rename d=delete a=admin g=get-only
|
||||
|
||||
[global]
|
||||
# -- Behind Caddy (real client IP) --
|
||||
# Trust the reverse proxy's X-Forwarded-For. Only Caddy can reach :3923 (it is
|
||||
# not published), so `lan` (trust private nets) is safe and robust; narrow it
|
||||
# to the exact docker subnet copyparty names in a startup warning if you like.
|
||||
xff-src: lan
|
||||
|
||||
# -- Security notices (copyparty's built-in vulnerability check) --
|
||||
# Periodically checks whether THIS version has a known advisory and warns in
|
||||
# the log / control panel. The companion update.sh acts on the same feed.
|
||||
vc-url: https://api.copyparty.eu/advisories
|
||||
vc-age: 3
|
||||
# vc-exit # opt-in: shut down if running a vulnerable version -- only
|
||||
# enable together with the security updater, or it will loop.
|
||||
|
||||
# -- Indexing / media --
|
||||
e2dsa # up2k database + filesystem scan (dedup, search)
|
||||
e2ts # parse media tags for the indexer
|
||||
|
||||
# -- SFTP + FTPS (direct listeners; NOT proxied by Caddy) --
|
||||
sftp: 3922 # SFTP server
|
||||
sftp-pw # allow password auth (not just ssh keys)
|
||||
ftps: 3990 # explicit-TLS FTPS (plaintext `ftp` intentionally omitted)
|
||||
ftp-pr: 12000-12099 # passive TCP port range (also published in docker-compose.yml)
|
||||
cert: /cfg/ftps.pem # self-signed by deploy.sh; swap in Caddy's LE cert to taste
|
||||
# ftp-nat: 203.0.113.10 # set to the server's public IP for passive FTPS via NAT
|
||||
|
||||
[accounts]
|
||||
# deploy.sh replaces __ADMIN_PW__ with a generated password on first run.
|
||||
admin: __ADMIN_PW__
|
||||
|
||||
# Default volume: the whole data root, private to admin. Open it up below.
|
||||
[/]
|
||||
/w
|
||||
accs:
|
||||
rwmda: admin
|
||||
|
||||
# --- examples (uncomment + adjust) -----------------------------------------
|
||||
# Public read-only share of /w/pub for anyone (no login):
|
||||
# [/pub]
|
||||
# /w/pub
|
||||
# accs:
|
||||
# r: *
|
||||
#
|
||||
# Anonymous write-only dropbox (uploads, no listing or download):
|
||||
# [/inbox]
|
||||
# /w/inbox
|
||||
# accs:
|
||||
# w: *
|
||||
#
|
||||
# Add another account: put `bob: s3cret` under [accounts] above, then grant it
|
||||
# access in a volume, e.g. `rw: bob` inside that volume's accs: block.
|
||||
@@ -0,0 +1,583 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# deploy.sh -- deploy copyparty (caddy + copyparty file server) on Alpine,
|
||||
# Debian or Alma. Single-node; runs as root.
|
||||
#
|
||||
# What this does:
|
||||
# 1. Installs docker + compose if missing.
|
||||
# 2. Lays down the stack files in $STACK_DIR.
|
||||
# 3. Generates cfg/copyparty.conf on first run (random admin password) and a
|
||||
# self-signed cfg/ftps.pem for FTPS; neither is overwritten on re-run.
|
||||
# 4. Generates .env on first run, pinning COPYPARTY_TAG to the newest release.
|
||||
# 5. Prompts for required values not preset (COPYPARTY_DOMAIN, ACME_EMAIL).
|
||||
# 6. Opens 80/443 + SFTP/FTPS/passive ports on the host firewall if present.
|
||||
# 7. Pulls images, brings the stack up, waits for health.
|
||||
# 8. Installs + schedules update.sh (security-notices-aware) unless disabled.
|
||||
#
|
||||
# Caddy fronts the web UI / WebDAV over TLS; SFTP and FTPS are published directly
|
||||
# by copyparty (not proxied). copyparty trusts Caddy's X-Forwarded-For so logs
|
||||
# and bans use the real client IP (see cfg/copyparty.conf `xff-src`).
|
||||
#
|
||||
# Idempotent: re-run to apply config changes / pull new images.
|
||||
#
|
||||
# Self-contained: docker-compose.yml, Caddyfile, copyparty.conf.example,
|
||||
# .env.example and update.sh are embedded as a base64 tar.gz at the bottom of
|
||||
# this file. Rebuild with build.sh after editing the loose source files.
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy.sh # interactive prompts
|
||||
# COPYPARTY_DOMAIN=files.example.com ACME_EMAIL=me@x.com \
|
||||
# SKIP_PROMPTS=1 bash deploy.sh # non-interactive
|
||||
# DATA_DIR=/mnt/disk/files FTP_NAT=203.0.113.10 bash deploy.sh
|
||||
# SKIP_DOCKER_INSTALL=1 bash deploy.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${STACK_DIR:=/srv/copyparty}"
|
||||
: "${SKIP_DOCKER_INSTALL:=0}"
|
||||
: "${FORCE:=0}"
|
||||
: "${SKIP_PROMPTS:=0}"
|
||||
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
|
||||
: "${COPYPARTY_DOMAIN:=}"
|
||||
: "${ACME_EMAIL:=}"
|
||||
: "${COPYPARTY_IMAGE:=copyparty/ac}"
|
||||
: "${COPYPARTY_TAG:=latest}"
|
||||
: "${PUID:=1000}"
|
||||
: "${PGID:=1000}"
|
||||
: "${DATA_DIR:=/srv/copyparty/data}"
|
||||
: "${BIND_ADDR:=}"
|
||||
: "${SFTP_PORT:=3922}"
|
||||
: "${FTPS_PORT:=3990}"
|
||||
: "${FTP_PASV_RANGE:=12000-12099}"
|
||||
: "${FTP_NAT:=}"
|
||||
: "${UPDATE_POLICY:=latest}"
|
||||
: "${VC_FEED:=advisories}"
|
||||
: "${COPYPARTY_AUTOUPDATE:=1}" # 0 = install update.sh but don't schedule it
|
||||
: "${CADDY_TAG:=2-alpine}"
|
||||
|
||||
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[[ $EUID -eq 0 ]] || die "Run as root."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OS detection + Docker install (Alpine / Debian / Alma). Inlined (this deploy.sh
|
||||
# is self-contained / scp'd standalone) rather than sourced from oslib.sh.
|
||||
# ---------------------------------------------------------------------------
|
||||
osfam() {
|
||||
local id="" like=""
|
||||
if [[ -r /etc/os-release ]]; then
|
||||
id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")"
|
||||
like="$(. /etc/os-release 2>/dev/null && echo "${ID_LIKE:-}")"
|
||||
fi
|
||||
case " $id $like " in
|
||||
*" alpine "*) echo alpine ;;
|
||||
*" debian "*|*" ubuntu "*) echo debian ;;
|
||||
*" rhel "*|*" fedora "*|*" centos "*) echo rhel ;;
|
||||
*) echo "${id:-unknown}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
pkg_install() { # best-effort install of a package across the three families
|
||||
case "$(osfam)" in
|
||||
alpine) apk add -q "$@" || true ;;
|
||||
debian) DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$@" || true ;;
|
||||
rhel) dnf install -y -q "$@" || true ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# fetch a URL to stdout with whatever's available (curl or wget).
|
||||
fetch() {
|
||||
if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" 2>/dev/null
|
||||
elif command -v wget >/dev/null 2>&1; then wget -qO- "$1" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
log "Docker already installed: $(docker --version)"
|
||||
else
|
||||
log "Installing Docker (OS: $(osfam))..."
|
||||
case "$(osfam)" in
|
||||
alpine) apk add -q docker docker-cli-compose openrc ;;
|
||||
debian|rhel) command -v curl >/dev/null 2>&1 || pkg_install curl
|
||||
curl -fsSL https://get.docker.com | sh ;;
|
||||
*) die "Unsupported OS for auto Docker install. Set SKIP_DOCKER_INSTALL=1 and install Docker yourself." ;;
|
||||
esac
|
||||
fi
|
||||
if command -v rc-update >/dev/null 2>&1; then
|
||||
rc-update add docker default >/dev/null 2>&1 || true
|
||||
rc-service docker status >/dev/null 2>&1 || rc-service docker start
|
||||
elif command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl enable --now docker >/dev/null 2>&1 || systemctl start docker || true
|
||||
fi
|
||||
}
|
||||
|
||||
open_web_ports() {
|
||||
# Register web (80/443) + SFTP/FTPS + the passive-FTP range. Prefer the host
|
||||
# firewall (harden-firewall.sh); else ufw/firewalld if active.
|
||||
#
|
||||
# NOTE: published Docker ports reach the host via nat/FORWARD and BYPASS the
|
||||
# INPUT firewall, so this is belt-and-braces + self-documentation. Pin the
|
||||
# real exposure with BIND_ADDR and copyparty's own access control.
|
||||
local pasv_colon="${FTP_PASV_RANGE/-/:}" # 12000-12099 -> 12000:12099 (iptables/ufw/ports.d)
|
||||
local pasv_hyphen="${FTP_PASV_RANGE}" # 12000-12099 (firewalld)
|
||||
if [[ -d /etc/firewall/ports.d && -x /usr/local/sbin/firewall-apply ]]; then
|
||||
log "Registering 80,443,${SFTP_PORT},${FTPS_PORT},${pasv_colon}/tcp with host firewall..."
|
||||
printf '80/tcp\n443/tcp\n%s/tcp\n%s/tcp\n%s/tcp\n' \
|
||||
"$SFTP_PORT" "$FTPS_PORT" "$pasv_colon" > /etc/firewall/ports.d/copyparty.rule
|
||||
/usr/local/sbin/firewall-apply
|
||||
elif command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
|
||||
log "ufw active -- allowing web + SFTP/FTPS..."
|
||||
local p
|
||||
for p in 80/tcp 443/tcp "${SFTP_PORT}/tcp" "${FTPS_PORT}/tcp" "${pasv_colon}/tcp"; do
|
||||
ufw allow "$p" >/dev/null
|
||||
done
|
||||
elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
|
||||
log "firewalld active -- allowing web + SFTP/FTPS..."
|
||||
firewall-cmd -q --add-service=http --permanent
|
||||
firewall-cmd -q --add-service=https --permanent
|
||||
firewall-cmd -q --add-port="${SFTP_PORT}/tcp" --permanent
|
||||
firewall-cmd -q --add-port="${FTPS_PORT}/tcp" --permanent
|
||||
firewall-cmd -q --add-port="${pasv_hyphen}/tcp" --permanent
|
||||
firewall-cmd -q --reload
|
||||
fi
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Extract embedded archive
|
||||
# ----------------------------------------------------------------------------
|
||||
SCRIPT_DIR=$(mktemp -d -t copyparty-deploy.XXXXXX)
|
||||
trap 'rm -rf "$SCRIPT_DIR"' EXIT
|
||||
|
||||
extract_archive() {
|
||||
grep -a -A 9999999 '^__ARCHIVE_BELOW__$' "$0" \
|
||||
| tail -n +2 \
|
||||
| base64 -d \
|
||||
| tar -xz -C "$SCRIPT_DIR"
|
||||
}
|
||||
|
||||
if grep -q -a '^__ARCHIVE_BELOW__$' "$0"; then
|
||||
log "Extracting embedded deployment files..."
|
||||
extract_archive
|
||||
else
|
||||
die "No embedded archive found. Run build.sh to embed deployment files."
|
||||
fi
|
||||
|
||||
EMBEDDED=(docker-compose.yml Caddyfile copyparty.conf.example .env.example update.sh)
|
||||
for f in "${EMBEDDED[@]}"; do
|
||||
[[ -f "$SCRIPT_DIR/$f" ]] || die "Embedded archive missing $f"
|
||||
done
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Prompt for required vars
|
||||
# ----------------------------------------------------------------------------
|
||||
prompt() {
|
||||
local varname="$1" message="$2"
|
||||
local -n ref="$varname"
|
||||
if [[ -z "${ref:-}" ]]; then
|
||||
[[ "$SKIP_PROMPTS" == "1" ]] && die "$varname required (set it in the environment; running with SKIP_PROMPTS=1)."
|
||||
read -r -p "$message: " ref
|
||||
[[ -n "$ref" ]] || die "$varname required."
|
||||
fi
|
||||
}
|
||||
|
||||
prompt COPYPARTY_DOMAIN "Public hostname for the web UI (e.g. files.example.com)"
|
||||
prompt ACME_EMAIL "Let's Encrypt email"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Docker + firewall
|
||||
# ----------------------------------------------------------------------------
|
||||
if [[ "$SKIP_DOCKER_INSTALL" != "1" ]]; then
|
||||
install_docker
|
||||
fi
|
||||
open_web_ports
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Stack directory + files
|
||||
# ----------------------------------------------------------------------------
|
||||
log "Setting up $STACK_DIR..."
|
||||
install -d -m 0750 "$STACK_DIR"
|
||||
install -m 0644 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml"
|
||||
install -m 0644 "$SCRIPT_DIR/Caddyfile" "$STACK_DIR/Caddyfile"
|
||||
install -m 0755 "$SCRIPT_DIR/update.sh" "$STACK_DIR/update.sh"
|
||||
install -d -m 0750 "$STACK_DIR/cfg"
|
||||
|
||||
# Data root (bind-mounted at /w), owned by the runtime UID/GID.
|
||||
install -d -m 0750 "$DATA_DIR"
|
||||
chown "$PUID:$PGID" "$DATA_DIR"
|
||||
|
||||
ENV_FILE="$STACK_DIR/.env"
|
||||
set_env() { # <KEY> <value>: update KEY in .env, or append if absent
|
||||
local key="$1" val="$2" esc
|
||||
esc=${val//\\/\\\\}; esc=${esc//|/\\|}; esc=${esc//&/\\&}
|
||||
if grep -qE "^${key}=" "$ENV_FILE"; then
|
||||
sed -i -e "s|^${key}=.*|${key}=${esc}|" "$ENV_FILE"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Pin COPYPARTY_TAG to the newest release so the running version is explicit.
|
||||
if [[ "$COPYPARTY_TAG" == "latest" || -z "$COPYPARTY_TAG" ]]; then
|
||||
_v="$(fetch "https://api.github.com/repos/9001/copyparty/releases/latest" \
|
||||
| grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]+"' | head -n1 \
|
||||
| sed -E 's/.*"v?([0-9][^"]*)".*/\1/')"
|
||||
if [[ -n "${_v:-}" ]]; then COPYPARTY_TAG="$_v"; log "Pinned COPYPARTY_TAG=${_v} (newest release)."
|
||||
else warn "Could not resolve the newest release; using tag 'latest'."; COPYPARTY_TAG="latest"; fi
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Generate copyparty.conf (admin password) + self-signed FTPS cert -- once.
|
||||
# ----------------------------------------------------------------------------
|
||||
CONF="$STACK_DIR/cfg/copyparty.conf"
|
||||
if [[ ! -f "$CONF" ]]; then
|
||||
ADMIN_PW="$(head -c 32 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 24)"
|
||||
log "Generating $CONF (admin account)..."
|
||||
install -m 0640 "$SCRIPT_DIR/copyparty.conf.example" "$CONF"
|
||||
sed -i "s|__ADMIN_PW__|${ADMIN_PW}|" "$CONF"
|
||||
if [[ -n "$FTP_NAT" ]]; then
|
||||
sed -i -E "s|^[[:space:]]*#[[:space:]]*ftp-nat:.*| ftp-nat: ${FTP_NAT}|" "$CONF"
|
||||
fi
|
||||
else
|
||||
log "$CONF exists; leaving it alone."
|
||||
fi
|
||||
# Read back the admin password for the summary (works on first run + re-runs).
|
||||
# Strip the leading 'admin:', any trailing inline #comment, and surrounding space.
|
||||
ADMIN_PW="$(grep -E '^[[:space:]]*admin:' "$CONF" | head -n1 \
|
||||
| sed -E 's/^[[:space:]]*admin:[[:space:]]*//; s/[[:space:]]*#.*$//; s/[[:space:]]*$//')"
|
||||
|
||||
FTPS_PEM="$STACK_DIR/cfg/ftps.pem"
|
||||
if [[ ! -f "$FTPS_PEM" ]]; then
|
||||
command -v openssl >/dev/null 2>&1 || { log "Installing openssl for the FTPS cert..."; pkg_install openssl; }
|
||||
command -v openssl >/dev/null 2>&1 || die "openssl is required to mint the FTPS cert; install it and re-run."
|
||||
log "Generating self-signed FTPS certificate (CN=${COPYPARTY_DOMAIN})..."
|
||||
_k="$(mktemp)"; _c="$(mktemp)"
|
||||
if ! openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
|
||||
-keyout "$_k" -out "$_c" -subj "/CN=${COPYPARTY_DOMAIN}" \
|
||||
-addext "subjectAltName=DNS:${COPYPARTY_DOMAIN}" 2>/dev/null; then
|
||||
# older openssl without -addext
|
||||
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
|
||||
-keyout "$_k" -out "$_c" -subj "/CN=${COPYPARTY_DOMAIN}"
|
||||
fi
|
||||
cat "$_k" "$_c" > "$FTPS_PEM" # copyparty --cert wants key + chain concatenated
|
||||
rm -f "$_k" "$_c"
|
||||
chmod 0640 "$FTPS_PEM"
|
||||
else
|
||||
log "$FTPS_PEM exists; leaving it."
|
||||
fi
|
||||
|
||||
# Everything copyparty (running as PUID) must read/write under /cfg.
|
||||
chown -R "$PUID:$PGID" "$STACK_DIR/cfg"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Seed .env (first run only)
|
||||
# ----------------------------------------------------------------------------
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
log "Seeding $ENV_FILE..."
|
||||
install -m 0600 "$SCRIPT_DIR/.env.example" "$ENV_FILE"
|
||||
set_env COPYPARTY_DOMAIN "$COPYPARTY_DOMAIN"
|
||||
set_env ACME_EMAIL "$ACME_EMAIL"
|
||||
set_env COPYPARTY_IMAGE "$COPYPARTY_IMAGE"
|
||||
set_env COPYPARTY_TAG "$COPYPARTY_TAG"
|
||||
set_env PUID "$PUID"
|
||||
set_env PGID "$PGID"
|
||||
set_env DATA_DIR "$DATA_DIR"
|
||||
set_env BIND_ADDR "$BIND_ADDR"
|
||||
set_env SFTP_PORT "$SFTP_PORT"
|
||||
set_env FTPS_PORT "$FTPS_PORT"
|
||||
set_env FTP_PASV_RANGE "$FTP_PASV_RANGE"
|
||||
set_env FTP_NAT "$FTP_NAT"
|
||||
set_env UPDATE_POLICY "$UPDATE_POLICY"
|
||||
set_env VC_FEED "$VC_FEED"
|
||||
set_env CADDY_TAG "$CADDY_TAG"
|
||||
else
|
||||
log ".env exists; leaving it alone."
|
||||
fi
|
||||
|
||||
# Validate required values landed.
|
||||
missing=()
|
||||
for var in COPYPARTY_DOMAIN ACME_EMAIL; do
|
||||
grep -E "^${var}=.+$" "$ENV_FILE" >/dev/null || missing+=("$var")
|
||||
done
|
||||
(( ${#missing[@]} == 0 )) || die "Missing values in $ENV_FILE: ${missing[*]}"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Confirm
|
||||
# ----------------------------------------------------------------------------
|
||||
if [[ "$FORCE" != "1" ]]; then
|
||||
cat <<EOF
|
||||
|
||||
About to pull images and start the copyparty stack from $STACK_DIR.
|
||||
|
||||
Caddy will request a Let's Encrypt cert for ${COPYPARTY_DOMAIN}. DNS for that
|
||||
name must already point at this host, and ports 80/443 must be reachable from
|
||||
the internet. SFTP (${SFTP_PORT}) and FTPS (${FTPS_PORT}) will also be opened.
|
||||
|
||||
Continue? [y/N]
|
||||
EOF
|
||||
read -r ans
|
||||
[[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] || { warn "Aborted."; exit 0; }
|
||||
fi
|
||||
|
||||
cd "$STACK_DIR"
|
||||
log "Pulling images..."
|
||||
docker compose pull
|
||||
|
||||
log "Starting stack..."
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Wait for health
|
||||
# ----------------------------------------------------------------------------
|
||||
log "Waiting for services to become healthy (up to 120s)..."
|
||||
deadline=$(( $(date +%s) + 120 ))
|
||||
while (( $(date +%s) < deadline )); do
|
||||
status=$(docker compose ps --format '{{.Service}} {{.Health}}' 2>/dev/null || true)
|
||||
unhealthy=$(echo "$status" | awk '$2 != "healthy" && $2 != "" {print $1}')
|
||||
if [[ -z "$unhealthy" && -n "$status" ]]; then
|
||||
log "All services healthy."
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Install + schedule the updater
|
||||
# ----------------------------------------------------------------------------
|
||||
if [[ "$COPYPARTY_AUTOUPDATE" != "0" ]]; then
|
||||
log "Scheduling the copyparty updater (policy=${UPDATE_POLICY})..."
|
||||
STACK_DIR="$STACK_DIR" UPDATE_POLICY="$UPDATE_POLICY" VC_FEED="$VC_FEED" \
|
||||
bash "$STACK_DIR/update.sh" install || warn "Updater scheduling failed (non-fatal)."
|
||||
else
|
||||
log "COPYPARTY_AUTOUPDATE=0 -- update.sh installed but not scheduled."
|
||||
fi
|
||||
|
||||
echo
|
||||
log "Stack status:"
|
||||
docker compose ps
|
||||
echo
|
||||
cat <<EOF
|
||||
================================================================
|
||||
DEPLOYED -- copyparty file server
|
||||
|
||||
Web UI: https://${COPYPARTY_DOMAIN}
|
||||
SFTP: sftp -P ${SFTP_PORT} admin@${COPYPARTY_DOMAIN}
|
||||
FTPS: explicit TLS on port ${FTPS_PORT}
|
||||
lftp -u admin -e 'set ftp:ssl-force true; set ssl:verify-certificate no' \\
|
||||
ftp://${COPYPARTY_DOMAIN}:${FTPS_PORT}
|
||||
Stack dir: ${STACK_DIR}
|
||||
|
||||
Login: user 'admin' / password '${ADMIN_PW}'
|
||||
(stored in ${STACK_DIR}/cfg/copyparty.conf)
|
||||
|
||||
Files: ${DATA_DIR} (mounted at /w; shared at the site root)
|
||||
|
||||
Notes:
|
||||
* FTPS uses a self-signed cert (${STACK_DIR}/cfg/ftps.pem). Clients must accept
|
||||
it, or swap in Caddy's Let's Encrypt cert -- see deployments/copyparty/README.md.
|
||||
* Passive FTPS through NAT needs FTP_NAT set to this host's public IP (re-run
|
||||
with FTP_NAT=..., or set ftp-nat in cfg/copyparty.conf).
|
||||
* Real client IP comes from Caddy via X-Forwarded-For (trusted with xff-src).
|
||||
|
||||
Add users / open shares:
|
||||
edit ${STACK_DIR}/cfg/copyparty.conf, then:
|
||||
docker compose exec copyparty kill -s USR1 1 # hot-reload, no restart
|
||||
|
||||
Updates (security-notices aware):
|
||||
${STACK_DIR}/update.sh check # current vs latest + any advisory match
|
||||
${STACK_DIR}/update.sh update # update now (TARGET_VERSION=x.y.z to pin)
|
||||
Scheduled daily; change UPDATE_POLICY (latest|security|off) in
|
||||
${STACK_DIR}/.env or /etc/copyparty-update.conf, or: update.sh uninstall
|
||||
|
||||
Manage:
|
||||
docker compose logs -f
|
||||
docker compose pull && docker compose up -d # manual update
|
||||
docker compose down # stop, keep data
|
||||
docker compose down -v # stop, keep host data dir
|
||||
|
||||
Or just re-run this script -- it's idempotent.
|
||||
================================================================
|
||||
EOF
|
||||
|
||||
# IMPORTANT: do not put any code below this exit. Everything after the
|
||||
# __ARCHIVE_BELOW__ marker is the embedded tar.gz payload (base64), appended by
|
||||
# build.sh.
|
||||
exit 0
|
||||
__ARCHIVE_BELOW__
|
||||
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
|
||||
VKYylalMZSrT/036L64TOhwAoAAA
|
||||
@@ -0,0 +1,78 @@
|
||||
# copyparty stack -- caddy (TLS) + copyparty (portable file server).
|
||||
#
|
||||
# Topology:
|
||||
# Browser/WebDAV --> caddy:443 --> copyparty:3923 (HTTP, internal)
|
||||
# SFTP / FTPS --> copyparty:{3922,3990,passive} (direct, NOT via caddy)
|
||||
#
|
||||
# Caddy terminates TLS and reverse-proxies the web/WebDAV UI. copyparty's SFTP
|
||||
# and FTPS listeners are their own TCP services, so they are published straight
|
||||
# from the copyparty container -- Caddy is HTTP-only and not in that path.
|
||||
#
|
||||
# The image's baked-in config does `chdir /w` + `% /cfg` (include every *.conf in
|
||||
# /cfg), so our ./cfg/copyparty.conf is picked up automatically -- no `command:`
|
||||
# override needed. XDG_CONFIG_HOME=/cfg in the image, so generated SSH host keys
|
||||
# live under /cfg and persist across restarts (the bind-mount is read-write).
|
||||
|
||||
name: copyparty
|
||||
|
||||
volumes:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Caddy -- TLS termination + reverse proxy for the web/WebDAV UI. The only
|
||||
# service on 80/443. Auto-issues a Let's Encrypt cert for ${COPYPARTY_DOMAIN}.
|
||||
# ---------------------------------------------------------------------------
|
||||
caddy:
|
||||
image: caddy:${CADDY_TAG:-2-alpine}
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp" # HTTP/3
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
environment:
|
||||
COPYPARTY_DOMAIN: "${COPYPARTY_DOMAIN}"
|
||||
ACME_EMAIL: "${ACME_EMAIL}"
|
||||
depends_on:
|
||||
- copyparty
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:2019/config/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# copyparty -- the file server. HTTP 3923 stays internal (Caddy fronts it);
|
||||
# only the direct SFTP/FTPS listeners are published. Runs as PUID:PGID so
|
||||
# files on the host bind-mount have sane ownership.
|
||||
#
|
||||
# A published Docker port BYPASSES the host INPUT firewall, so set BIND_ADDR
|
||||
# to pin these listeners to a trusted interface if the box is multi-homed.
|
||||
# ---------------------------------------------------------------------------
|
||||
copyparty:
|
||||
image: ${COPYPARTY_IMAGE:-copyparty/ac}:${COPYPARTY_TAG:-latest}
|
||||
container_name: copyparty
|
||||
restart: unless-stopped
|
||||
user: "${PUID:-1000}:${PGID:-1000}"
|
||||
ports:
|
||||
- "${BIND_ADDR:-0.0.0.0}:${SFTP_PORT:-3922}:3922" # SFTP
|
||||
- "${BIND_ADDR:-0.0.0.0}:${FTPS_PORT:-3990}:3990" # FTPS (explicit TLS)
|
||||
- "${BIND_ADDR:-0.0.0.0}:${FTP_PASV_RANGE:-12000-12099}:${FTP_PASV_RANGE:-12000-12099}" # FTPS passive
|
||||
volumes:
|
||||
- ${DATA_DIR:-/srv/copyparty/data}:/w
|
||||
- ./cfg:/cfg
|
||||
healthcheck:
|
||||
# copyparty's own python -- no extra deps in the image. A successful TCP
|
||||
# connect to the HTTP listener means the server is up.
|
||||
test: ["CMD", "python3", "-c", "import socket; socket.create_connection(('127.0.0.1',3923),3).close()"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
@@ -0,0 +1,439 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# update.sh -- update the copyparty container, optionally driven by copyparty's
|
||||
# security-notices API. Companion to deploy.sh; installed alongside the stack.
|
||||
#
|
||||
# copyparty does NOT self-update, but it DOES publish a machine-readable feed of
|
||||
# security advisories (the same one its built-in `--vc-url` check uses). This
|
||||
# script reads that feed to decide when to act, pins the new version in .env,
|
||||
# and recreates the container -- so the running version is always explicit and
|
||||
# a bad release can be rolled back by editing .env.
|
||||
#
|
||||
# Subcommands:
|
||||
# check (default) report current vs latest + any matching advisory; no changes
|
||||
# run apply per UPDATE_POLICY, then notify (this is what the schedule runs)
|
||||
# update force an update now (to latest, or TARGET_VERSION=x.y.z)
|
||||
# install schedule the daily `run`
|
||||
# uninstall remove the schedule
|
||||
#
|
||||
# Policy (UPDATE_POLICY):
|
||||
# latest update to the newest release whenever one exists (default)
|
||||
# security update ONLY when the running version has a known advisory, to the
|
||||
# patched release named in the feed
|
||||
# off never change the running version (check/notify only)
|
||||
#
|
||||
# Env (also read from /etc/copyparty-update.conf; environment wins):
|
||||
# STACK_DIR=/srv/copyparty UPDATE_POLICY=latest VC_FEED=advisories
|
||||
# NOTIFY=1 SSH_NOTIFY_CONF=/etc/ssh-notify.conf DRY_RUN=0
|
||||
# TARGET_VERSION= GH_REPO=9001/copyparty
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||
|
||||
# Load defaults from the conf for any var not already set in the environment
|
||||
# (precedence: environment > conf > built-in).
|
||||
: "${COPYPARTY_UPDATE_CONF:=/etc/copyparty-update.conf}"
|
||||
if [[ -r "$COPYPARTY_UPDATE_CONF" ]]; then
|
||||
while IFS= read -r _line; do
|
||||
[[ "$_line" =~ ^[[:space:]]*# || -z "${_line//[[:space:]]/}" ]] && continue
|
||||
_k="${_line%%=*}"; _v="${_line#*=}"; _k="${_k//[[:space:]]/}"
|
||||
[[ "$_k" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
||||
[[ -n "${!_k:-}" ]] && continue
|
||||
_v="${_v%\"}"; _v="${_v#\"}"
|
||||
printf -v "$_k" '%s' "$_v"
|
||||
done < "$COPYPARTY_UPDATE_CONF"
|
||||
fi
|
||||
|
||||
: "${STACK_DIR:=/srv/copyparty}"
|
||||
: "${UPDATE_POLICY:=latest}"
|
||||
: "${VC_FEED:=advisories}"
|
||||
: "${NOTIFY:=1}"
|
||||
: "${SSH_NOTIFY_CONF:=/etc/ssh-notify.conf}"
|
||||
: "${DRY_RUN:=0}"
|
||||
: "${TARGET_VERSION:=}"
|
||||
: "${GH_REPO:=9001/copyparty}"
|
||||
: "${LOG:=/var/log/copyparty-update.log}"
|
||||
|
||||
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
run() { if [[ "$DRY_RUN" == "1" ]]; then echo "DRY: $*"; else eval "$@"; fi; }
|
||||
|
||||
# docker compose, scoped to the stack dir (so ./Caddyfile, ./cfg resolve and
|
||||
# .env is auto-loaded).
|
||||
dc() { ( cd "$STACK_DIR" && docker compose "$@" ); }
|
||||
|
||||
osfam() {
|
||||
local id="" like=""
|
||||
if [[ -r /etc/os-release ]]; then
|
||||
id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")"
|
||||
like="$(. /etc/os-release 2>/dev/null && echo "${ID_LIKE:-}")"
|
||||
fi
|
||||
case " $id $like " in
|
||||
*" alpine "*) echo alpine ;;
|
||||
*" debian "*|*" ubuntu "*) echo debian ;;
|
||||
*" rhel "*|*" fedora "*|*" centos "*) echo rhel ;;
|
||||
*) echo "${id:-unknown}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# .env helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
ENV_FILE="$STACK_DIR/.env"
|
||||
env_get() { [[ -f "$ENV_FILE" ]] && grep -E "^$1=" "$ENV_FILE" | head -n1 | cut -d= -f2- || true; }
|
||||
env_set() { # <KEY> <value>
|
||||
local key="$1" val="$2" esc
|
||||
esc=${val//\\/\\\\}; esc=${esc//|/\\|}; esc=${esc//&/\\&}
|
||||
if grep -qE "^${key}=" "$ENV_FILE"; then
|
||||
sed -i -e "s|^${key}=.*|${key}=${esc}|" "$ENV_FILE"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# strip a leading v and anything after the X.Y.Z core
|
||||
normver() { printf '%s' "$1" | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1; }
|
||||
# $1 <= $2 ?
|
||||
ver_le() { [[ "$1" == "$2" ]] || [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" == "$1" ]]; }
|
||||
ver_gt() { ! ver_le "$1" "$2"; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Version discovery
|
||||
# ---------------------------------------------------------------------------
|
||||
# Currently-running version: prefer the live container, fall back to the pinned
|
||||
# tag in .env.
|
||||
current_version() {
|
||||
local v=""
|
||||
v="$(dc exec -T copyparty python3 -m copyparty --versionb 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)"
|
||||
if [[ -z "$v" ]]; then v="$(normver "$(env_get COPYPARTY_TAG)")"; fi
|
||||
printf '%s' "$v"
|
||||
}
|
||||
|
||||
# Newest released version from the GitHub releases API (tag_name, e.g. v1.20.11).
|
||||
latest_version() {
|
||||
command -v curl >/dev/null 2>&1 || { warn "curl missing; cannot resolve latest release."; return 0; }
|
||||
curl -fsSL "https://api.github.com/repos/${GH_REPO}/releases/latest" 2>/dev/null \
|
||||
| grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]+"' | head -n1 \
|
||||
| sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v?([^"]+)".*/\1/'
|
||||
}
|
||||
|
||||
# Advisory scan: ask copyparty's own python (inside the container -- no host jq/
|
||||
# python dependency) whether $1 is covered by any advisory in $VC_FEED. Prints:
|
||||
# OK
|
||||
# ERR <msg>
|
||||
# VULN <max-patched-version>\n<comma-separated advisory ids>
|
||||
advisory_scan() {
|
||||
local ver="$1" feed="https://api.copyparty.eu/${VC_FEED}"
|
||||
dc exec -T -e CPV="$ver" -e FEED="$feed" copyparty python3 - <<'PY' 2>/dev/null || echo "ERR exec"
|
||||
import json, os, re, sys, urllib.request
|
||||
cpv = os.environ.get("CPV", "")
|
||||
feed = os.environ.get("FEED", "")
|
||||
def vt(s):
|
||||
out = []
|
||||
for p in re.split(r'[.\-]', s.strip().lstrip("vV")):
|
||||
m = re.match(r'\d+', p)
|
||||
out.append(int(m.group(0)) if m else 0)
|
||||
return tuple(out)
|
||||
def cmp(a, b):
|
||||
ta, tb = vt(a), vt(b)
|
||||
n = max(len(ta), len(tb))
|
||||
ta += (0,) * (n - len(ta)); tb += (0,) * (n - len(tb))
|
||||
return (ta > tb) - (ta < tb)
|
||||
def satisfies(ver, rng):
|
||||
for part in rng.split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
m = re.match(r'(<=|>=|==|=|<|>)?\s*v?([0-9][0-9A-Za-z.\-]*)', part)
|
||||
if not m:
|
||||
return False
|
||||
op = m.group(1) or "="
|
||||
c = cmp(ver, m.group(2))
|
||||
ok = ((op in ("=", "==") and c == 0) or (op == "<" and c < 0) or
|
||||
(op == "<=" and c <= 0) or (op == ">" and c > 0) or (op == ">=" and c >= 0))
|
||||
if not ok:
|
||||
return False
|
||||
return True
|
||||
if not cpv:
|
||||
print("ERR no-version"); sys.exit(0)
|
||||
try:
|
||||
raw = urllib.request.urlopen(feed, timeout=10).read()
|
||||
data = json.loads(raw)
|
||||
except Exception as e:
|
||||
print("ERR " + str(e)[:80]); sys.exit(0)
|
||||
if isinstance(data, dict):
|
||||
data = data.get("advisories") or data.get("data") or [data]
|
||||
hits, patched = [], []
|
||||
for adv in data:
|
||||
for v in (adv.get("vulnerabilities") or []):
|
||||
name = ((v.get("package") or {}).get("name") or "").lower()
|
||||
if name and name != "copyparty":
|
||||
continue
|
||||
rng = v.get("vulnerable_version_range") or ""
|
||||
if rng and satisfies(cpv, rng):
|
||||
hits.append(adv.get("ghsa_id") or adv.get("cve_id") or "advisory")
|
||||
pv = v.get("patched_versions") or v.get("first_patched_version") or ""
|
||||
if isinstance(pv, dict):
|
||||
pv = pv.get("identifier", "")
|
||||
pv = re.sub(r'^[^0-9]*', '', str(pv))
|
||||
if pv:
|
||||
patched.append(pv)
|
||||
if not hits:
|
||||
print("OK")
|
||||
else:
|
||||
best = ""
|
||||
for p in patched:
|
||||
if not best or cmp(p, best) > 0:
|
||||
best = p
|
||||
print("VULN " + best)
|
||||
print(",".join(sorted(set(hits))))
|
||||
PY
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Notify (reuse the login-notifier's ntfy config, like scripts/auto-update.sh)
|
||||
# ---------------------------------------------------------------------------
|
||||
send_notice() { # <title> <priority> <body>
|
||||
[[ "$NOTIFY" == "1" ]] || return 0
|
||||
[[ -r "$SSH_NOTIFY_CONF" ]] || return 0
|
||||
# shellcheck disable=SC1090
|
||||
. "$SSH_NOTIFY_CONF"
|
||||
[[ -n "${NTFY_URL:-}" ]] || return 0
|
||||
command -v curl >/dev/null 2>&1 || return 0
|
||||
local title="$1" prio="$2" body="$3" host
|
||||
host="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo unknown)"
|
||||
set -- -fsS -m 5 -H "X-Title: ${title}" -H "X-Priority: ${prio}"
|
||||
[[ -n "${NTFY_TOKEN:-}" ]] && set -- "$@" -H "Authorization: Bearer ${NTFY_TOKEN}"
|
||||
[[ -n "${NTFY_EMAIL:-}" ]] && set -- "$@" -H "X-Email: ${NTFY_EMAIL}"
|
||||
local t="copyparty"; [[ -n "${NTFY_REGION:-}" ]] && t="${t},${NTFY_REGION}"
|
||||
set -- "$@" -H "X-Tags: ${t}"
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
echo "DRY: curl ntfy ($prio): $body"
|
||||
else
|
||||
curl "$@" -d "${body} [${host}]" "$NTFY_URL" >/dev/null 2>&1 || true
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Health wait (same shape as deploy.sh)
|
||||
# ---------------------------------------------------------------------------
|
||||
wait_health() {
|
||||
local deadline; deadline=$(( $(date +%s) + 120 ))
|
||||
while (( $(date +%s) < deadline )); do
|
||||
local status unhealthy
|
||||
status="$(dc ps --format '{{.Service}} {{.Health}}' 2>/dev/null || true)"
|
||||
unhealthy="$(echo "$status" | awk '$2 != "healthy" && $2 != "" {print $1}')"
|
||||
[[ -z "$unhealthy" && -n "$status" ]] && return 0
|
||||
sleep 5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Apply an update: pin the tag, pull, recreate, verify; roll back on failure.
|
||||
# ---------------------------------------------------------------------------
|
||||
apply_update() { # <from> <to> [advisory-ids]
|
||||
local from="$1" to="$2" advs="${3:-}"
|
||||
log "Updating copyparty ${from:-?} -> ${to}..."
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
echo "DRY: set COPYPARTY_TAG=${to}; docker compose pull copyparty; docker compose up -d"
|
||||
return 0
|
||||
fi
|
||||
cp -a "$ENV_FILE" "${ENV_FILE}.bak.$(date -u +%Y%m%d%H%M%S)" 2>/dev/null || true
|
||||
env_set COPYPARTY_TAG "$to"
|
||||
if ! dc pull copyparty; then
|
||||
warn "pull failed; restoring COPYPARTY_TAG=${from}."
|
||||
env_set COPYPARTY_TAG "$from"
|
||||
send_notice "copyparty update FAILED" "high" "pull of ${to} failed; staying on ${from}"
|
||||
return 1
|
||||
fi
|
||||
dc up -d --remove-orphans
|
||||
if wait_health; then
|
||||
log "copyparty ${to} is healthy."
|
||||
local b="updated ${from:-?} -> ${to}"; [[ -n "$advs" ]] && b="${b} (advisory: ${advs})"
|
||||
send_notice "copyparty updated" "default" "$b"
|
||||
return 0
|
||||
fi
|
||||
warn "copyparty ${to} did not become healthy; rolling back to ${from}."
|
||||
env_set COPYPARTY_TAG "$from"
|
||||
dc up -d --remove-orphans || true
|
||||
send_notice "copyparty update FAILED" "high" "${to} unhealthy; rolled back to ${from}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Decide the target version for the current policy. Sets globals TARGET,
|
||||
# TARGET_IDS and SCAN_NOTE (NOT via stdout -- a $() subshell would lose them).
|
||||
# ---------------------------------------------------------------------------
|
||||
TARGET=""; TARGET_IDS=""; SCAN_NOTE=""
|
||||
resolve_target() { # <current-version>
|
||||
local cur="$1"
|
||||
TARGET=""; TARGET_IDS=""; SCAN_NOTE=""
|
||||
if [[ -n "$TARGET_VERSION" ]]; then
|
||||
TARGET="$(normver "$TARGET_VERSION")"; SCAN_NOTE="target override ${TARGET}"; return 0
|
||||
fi
|
||||
case "$UPDATE_POLICY" in
|
||||
off)
|
||||
SCAN_NOTE="policy=off (no changes)"; return 0 ;;
|
||||
security)
|
||||
local scan first rest
|
||||
scan="$(advisory_scan "$cur")"
|
||||
first="$(printf '%s' "$scan" | head -n1)"
|
||||
rest="$(printf '%s' "$scan" | sed -n '2p')"
|
||||
case "$first" in
|
||||
VULN*)
|
||||
TARGET="$(printf '%s' "$first" | awk '{print $2}')"
|
||||
TARGET_IDS="$rest"
|
||||
SCAN_NOTE="VULNERABLE (${rest:-?}); patched in ${TARGET:-?}"
|
||||
# if the feed names no patched version, fall forward to latest
|
||||
[[ -z "$TARGET" ]] && TARGET="$(normver "$(latest_version)")" ;;
|
||||
OK) SCAN_NOTE="no advisory matches ${cur}" ;;
|
||||
*) SCAN_NOTE="advisory check unavailable (${first})" ;;
|
||||
esac ;;
|
||||
latest|*)
|
||||
TARGET="$(normver "$(latest_version)")"
|
||||
SCAN_NOTE="policy=latest" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subcommands
|
||||
# ---------------------------------------------------------------------------
|
||||
preflight() {
|
||||
[[ $EUID -eq 0 ]] || die "Run as root."
|
||||
[[ -f "$STACK_DIR/docker-compose.yml" ]] || die "No stack at $STACK_DIR (set STACK_DIR)."
|
||||
command -v docker >/dev/null 2>&1 || die "docker not found."
|
||||
}
|
||||
|
||||
do_check() {
|
||||
preflight
|
||||
local cur latest scan
|
||||
cur="$(current_version)"
|
||||
latest="$(normver "$(latest_version)")"
|
||||
log "Running version: ${cur:-unknown} | latest release: ${latest:-unknown} | policy: ${UPDATE_POLICY}"
|
||||
scan="$(advisory_scan "$cur")"
|
||||
local first ids; first="$(printf '%s' "$scan" | head -n1)"; ids="$(printf '%s' "$scan" | sed -n '2p')"
|
||||
local prio="min" note=""
|
||||
case "$first" in
|
||||
VULN*) note="VULNERABLE: ${ids} (patched ${first#VULN })"; prio="high"; warn "$note" ;;
|
||||
OK) note="no known advisory affects ${cur:-?}"; log "$note" ;;
|
||||
*) note="advisory check unavailable (${first})"; warn "$note" ;;
|
||||
esac
|
||||
if [[ -n "$cur" && -n "$latest" ]] && ver_gt "$latest" "$cur"; then
|
||||
log "A newer release is available: ${cur} -> ${latest}"
|
||||
[[ "$prio" == "min" ]] && prio="default"
|
||||
note="${note}; newer release ${latest} available"
|
||||
fi
|
||||
send_notice "copyparty check" "$prio" "${note}"
|
||||
}
|
||||
|
||||
do_run() {
|
||||
preflight
|
||||
[[ "$DRY_RUN" == "1" ]] || { install -d -m 0755 "$(dirname "$LOG")" 2>/dev/null || true; echo "=== copyparty-update $(date -u +%FT%TZ) ===" >> "$LOG"; }
|
||||
local cur
|
||||
cur="$(current_version)"
|
||||
resolve_target "$cur"
|
||||
log "current=${cur:-?} | ${SCAN_NOTE}"
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
log "No update to apply."
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "$cur" ]] && ! ver_gt "$TARGET" "$cur"; then
|
||||
log "Target ${TARGET} is not newer than ${cur}; nothing to do."
|
||||
return 0
|
||||
fi
|
||||
apply_update "$cur" "$TARGET" "$TARGET_IDS"
|
||||
}
|
||||
|
||||
do_update() {
|
||||
# force: default to latest unless TARGET_VERSION/policy says otherwise
|
||||
preflight
|
||||
local cur
|
||||
cur="$(current_version)"
|
||||
if [[ -z "$TARGET_VERSION" && "$UPDATE_POLICY" == "off" ]]; then UPDATE_POLICY=latest; fi
|
||||
resolve_target "$cur"
|
||||
[[ -n "$TARGET" ]] || die "Could not determine a target version (${SCAN_NOTE})."
|
||||
if [[ -n "$cur" ]] && ! ver_gt "$TARGET" "$cur" && [[ -z "$TARGET_VERSION" ]]; then
|
||||
log "Already on ${cur} (latest ${TARGET}); nothing to do."
|
||||
return 0
|
||||
fi
|
||||
apply_update "$cur" "$TARGET" "$TARGET_IDS"
|
||||
}
|
||||
|
||||
# Write /etc/copyparty-update.conf so the scheduled run inherits these.
|
||||
write_conf() {
|
||||
cat > "$COPYPARTY_UPDATE_CONF" <<CONF
|
||||
# Defaults for the scheduled copyparty updater (deployments/copyparty/update.sh).
|
||||
# Environment variables still override these at runtime.
|
||||
STACK_DIR="${STACK_DIR}"
|
||||
UPDATE_POLICY="${UPDATE_POLICY}"
|
||||
VC_FEED="${VC_FEED}"
|
||||
NOTIFY="${NOTIFY}"
|
||||
CONF
|
||||
chmod 644 "$COPYPARTY_UPDATE_CONF"
|
||||
log "Wrote $COPYPARTY_UPDATE_CONF"
|
||||
}
|
||||
|
||||
do_install() {
|
||||
[[ $EUID -eq 0 ]] || die "Run as root."
|
||||
write_conf
|
||||
case "$(osfam)" in
|
||||
alpine)
|
||||
install -d -m 0755 /etc/periodic/daily
|
||||
cat > /etc/periodic/daily/copyparty-update <<EOF
|
||||
#!/bin/sh
|
||||
exec bash "$SELF" run
|
||||
EOF
|
||||
chmod +x /etc/periodic/daily/copyparty-update
|
||||
command -v rc-update >/dev/null 2>&1 && { rc-update add crond default >/dev/null 2>&1 || true; rc-service crond start >/dev/null 2>&1 || true; }
|
||||
log "Scheduled daily via /etc/periodic/daily/copyparty-update (policy=${UPDATE_POLICY})." ;;
|
||||
*)
|
||||
cat > /etc/systemd/system/copyparty-update.service <<EOF
|
||||
[Unit]
|
||||
Description=copyparty container updater
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/env bash $SELF run
|
||||
EOF
|
||||
cat > /etc/systemd/system/copyparty-update.timer <<EOF
|
||||
[Unit]
|
||||
Description=Daily copyparty update check
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
Persistent=true
|
||||
RandomizedDelaySec=1h
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now copyparty-update.timer >/dev/null 2>&1 || true
|
||||
log "Scheduled daily via systemd timer copyparty-update.timer (policy=${UPDATE_POLICY})." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
do_uninstall() {
|
||||
[[ $EUID -eq 0 ]] || die "Run as root."
|
||||
rm -f /etc/periodic/daily/copyparty-update
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now copyparty-update.timer >/dev/null 2>&1 || true
|
||||
rm -f /etc/systemd/system/copyparty-update.timer /etc/systemd/system/copyparty-update.service
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
log "Removed the scheduled copyparty updater."
|
||||
}
|
||||
|
||||
case "${1:-check}" in
|
||||
check) do_check ;;
|
||||
run) do_run ;;
|
||||
update) do_update ;;
|
||||
install) do_install ;;
|
||||
uninstall) do_uninstall ;;
|
||||
*) die "Usage: update.sh [check|run|update|install|uninstall]" ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user