Skip to content

Rewrite proxmox.sh: replace apt-sync+lftp with tsumugu + /iso parser - #206

Open
yaoge123 wants to merge 1 commit into
tuna:masterfrom
yaoge123:rewrite-proxmox-sh
Open

yaoge123 wants to merge 1 commit into
tuna:masterfrom
yaoge123:rewrite-proxmox-sh

Conversation

@yaoge123

@yaoge123 yaoge123 commented May 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Delete the legacy apt-sync + lftp based proxmox.sh implementation and split the mirror into two stages with different runtime requirements:

  • proxmox-deb-img.sh (new) — mirrors /debian/ and /images/ with tsumugu (runs in the tunathu/tsumugu image).
  • proxmox-iso.py (new) — stdlib-only Python scraper for the custom /iso/ HTML page (runs in the tunathu/tunasync-scripts image).
  • proxmox.sh (kept) — backward-compatible wrapper: it runs every stage the current image provides (tsumugu → deb/images, python3 → iso), prints a loud stderr warning for each skipped stage, and exits non-zero when no stage can run.

Why

  1. /iso/ no longer serves a machine-readable autoindex — it is a custom HTML download page, so lftp mirror cannot enumerate it. tsumugu's nginx/apache parsers do not parse it either, and its fallback parser reports ISO sizes as 0, so /iso/ gets a small purpose-built scraper instead.
  2. The apt-sync per-repo orchestration is replaced by tsumugu's crawler with explicit include/exclude scope rules.
  3. Two scripts are needed because no stock image ships both tsumugu and python3 (tunathu/tsumugu has no python3, tunathu/tunasync-scripts has no tsumugu).

Layout

Workdir layout is unchanged: debian/, images/, iso/ sit under TUNASYNC_WORKING_DIR. (pmg, pve, devel are repositories under debian/, not siblings of iso/.)

Deployment: two jobs, one shared workdir

[[mirrors]]
name = "proxmox"
provider = "command"
upstream = "http://download.proxmox.com/"
command = "/home/proxmox-deb-img.sh"
docker_image = "tunathu/tsumugu"
docker_volumes = ["/path/to/proxmox-deb-img.sh:/home/proxmox-deb-img.sh:ro"]

[[mirrors]]
name = "proxmox-iso"
provider = "command"
upstream = "http://download.proxmox.com/"
command = "/home/proxmox-iso.py"
docker_image = "tunathu/tunasync-scripts:latest"
docker_volumes = ["/path/to/proxmox-iso.py:/home/proxmox-iso.py:ro"]

Both jobs must share the same TUNASYNC_WORKING_DIR (the mirror root; do not point mirror_dir at the iso/ subdirectory). Legacy single-job configs invoking proxmox.sh keep working via the wrapper: it runs every stage the image supports, warns on stderr for each skipped stage, and exits non-zero when no stage can run.

/debian/ scope

The default scope matches the legacy apt-sync script: repos pve, pbs, pbs-client, pmg (all suites published for them — currently bullseye, bookworm, trixie, the same set as apt-sync.py's @debian-current), amd64 only, plus the top-level files (key.asc, *.gpg).

PROXMOX_DEB_ALL=1 opts into the full /debian/ tree (adds ceph-*, corosync-3, pdm, devel, all architectures). Note that tsumugu treats excluded remote paths as absent upstream, so narrowing the scope on an existing full mirror deletes the extra local trees (bounded by --max-delete, which aborts the run when exceeded) — dry-run first via TUNASYNC_TSUMUGU_OPTIONS=--dry-run.

debian/dists -> pve/dists is created as a symlink before syncing (as the legacy script did): upstream /debian/dists/ is a real directory duplicating the pve .debs, and creating the symlink first lets tsumugu record it in its keep-set so fresh installs never download a second copy.

/iso/ stage

  • Mirrors only the top-level listing: upstream /iso/ is flat today; subdirectory links are skipped and the scraper would need extending if upstream ever adds them (the old lftp script mirrored recursively).
  • Freshness compares size (when Content-Length is sent) AND Last-Modified vs the local mtime (when sent) — same-size replacements (re-signed .asc, re-spun ISO) are re-fetched. HEAD-rejecting servers are handled with a 1-byte ranged GET fallback.
  • Downloads go to .tmp.<name> then rename atomically; leftover .tmp.* is removed at startup; the local mtime is set to the upstream Last-Modified.
  • Downloaded payloads are verified against their published .sha256 (fetched first); a mismatch removes both sides of the pair and fails the run.
  • Stale deletes are bounded by TUNASYNC_PROXMOX_ISO_MAXDELETE (default 100) and only run after a fully successful pass.
  • Redirects are refused unless the final URL keeps an http(s) scheme, the upstream host, and a path under /iso/. The allowlist is derived from TUNASYNC_UPSTREAM_URL, so pointing the job at another mirror/proxy host works.

Size reporting

New for this mirror (the legacy script reported none): the deb-img stage sums the trees into REPO_SIZE_FILE and prints the size-sum: line via helpers/size-sum.sh — that helper already exists in this repo and is shared by other scripts (bazel-apt.sh, chef.sh, cvmfs.sh, …); production images that mount only this single script fall back to a direct numfmt print.

Testing

  • tsumugu sync --dry-run against live upstream with the default scope: ~48.5k objects, ~398 GiB apparent; debian/dists kept as a symlink, never descended into.
  • Stub-based integration tests for proxmox-iso.py: 18 cases — fresh download, skip on matching size+mtime, changed size, changed mtime, sha256 mismatch abort, redirect allowlist (cross-host, non-http scheme, path escape), leftover .tmp cleanup, stale-delete cap, HEAD-rejecting server, path-prefixed upstream, …
  • The wrapper was exercised in 5 cases: both stages present, deb-only, iso-only, neither (non-zero exit), stage-failure propagation.

Copilot AI review requested due to automatic review settings May 24, 2026 09:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates the Proxmox mirror sync wrapper to safely synchronize /debian and /images via tsumugu, while handling the non-indexed /iso page with a custom HTML parser/downloader to avoid incorrect parsing and unsafe deletions.

Changes:

  • Replace prior apt-sync/lftp approach with tsumugu sync scoped per subtree for safe cleanup behavior.
  • Add an embedded Python /iso sync stage that discovers artifacts from the custom HTML page, performs HEAD metadata checks, downloads atomically, and deletes stale files with a bounded limit.
  • Add extensive in-script rationale documenting why /iso must be handled separately.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread proxmox.sh Outdated
Comment thread proxmox.sh Outdated
Comment thread proxmox.sh Outdated
Comment thread proxmox-iso.py
Comment thread proxmox-iso.py
Comment thread proxmox-iso.py
Comment thread proxmox-iso.py Outdated
Comment thread proxmox.sh
@yaoge123

Copy link
Copy Markdown
Contributor Author

Reworked from production experience: the previous single proxmox.sh could not run in either stock image (tunathu/tsumugu has no python3, tunathu/tunasync-scripts has no tsumugu). Now split into two jobs (2ea1ec0): proxmox-deb-img.sh syncs /debian/ and /images/ with tsumugu, proxmox-iso.py scrapes the custom /iso/ page with stdlib-only python (atomic .tmp+rename downloads, stale deletion capped by TUNASYNC_PROXMOX_ISO_MAXDELETE). Both have been running in production on mirror.nju.edu.cn.

@yaoge123

Copy link
Copy Markdown
Contributor Author

Copilot review addressed:

  1. Missing pipefail — fixed: set -euo pipefail in proxmox-deb-img.sh.
  2. Computed USERAGENT not reaching the Python /iso stage — no longer applicable after the rewrite: the shell stage no longer derives a UA from tsumugu --version; both stages read TUNASYNC_TSUMUGU_USERAGENT with the same default (tsumugu), so they agree by construction. (The deb-img and iso stages now run as separate tunasync jobs, so a shell-side export could not reach the Python process anyway.)
  3. Hard-coded download.proxmox.com allowlist — fixed (867ae01): the allowed netloc and the required path prefix are derived from the configured TUNASYNC_UPSTREAM_URL, so pointing the job at another mirror/proxy host no longer silently skips every link.
  4. HEAD rejected (405/403) failing the initial sync — fixed (867ae01): when HEAD fails the script falls back to a 1-byte ranged GET and takes the size from Content-Range (or Content-Length when Range is ignored). Verified end-to-end against a local test server in HEAD-rejecting, HEAD-allowing, and path-prefixed upstream modes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread proxmox-iso.py
Comment thread proxmox-deb-img.sh
Comment thread proxmox-deb-img.sh
Comment thread proxmox-iso.py

@happyaron happyaron left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this — the old script clearly doesn't cope with the current upstream layout. A few things need addressing before this can go in:

1. The PR description doesn't match the diff. It describes a single rewritten proxmox.sh (42 → 219 lines), but the diff deletes proxmox.sh and adds two separate jobs (proxmox-deb-img.sh + proxmox-iso.py). It also says /pmg, /pve, /devel are siblings of /iso (they're under debian/), that size reporting via REPO_SIZE_FILE / helpers/size-sum.sh is preserved (that helper doesn't exist in this repo, and neither new script reports size), and that tsumugu handles /iso with a parser hint (the PR adds a Python scraper precisely because it doesn't). Please update it so reviewers know what they're approving.

2. Deleting proxmox.sh breaks existing deployments. Every mirror whose tunasync config points at proxmox.sh will start failing after pulling this. The replacement also requires config changes that aren't documented anywhere: two jobs, two different images (tunathu/tsumugu and tunathu/tunasync-scripts), sharing one working dir. Please either keep proxmox.sh as the entry point or document the new job setup. See also the inline comment on scope/size.

3. /iso is no longer synced recursively. The old lftp mirror was recursive; the new scraper only reads the top-level page. Fine while /iso/ stays flat, but worth stating explicitly.

Comment thread proxmox-deb-img.sh
--exclude '/pmg/dists/.+changelog$' \
--exclude '^/dists/trixie/pve-test/binary-arm64/' \
--exclude '^/pve/dists/trixie/pve-test/binary-arm64/' \
"${UPSTREAM%/}/debian/" "$WORKDIR/debian"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mirrors all of /debian/, which is a big scope increase over the old script (pve / pbs / pbs-client / pmg only, current Debian release only, amd64 only). Upstream /debian/ currently also includes ceph-octopus … ceph-tentacle, corosync-3, pdm, devel, and bullseye/bookworm/trixie across all architectures.

On top of that, on a fresh install the pve packages get stored twice: upstream /debian/dists/ is a real directory holding the .debs themselves (Packages has Filename: dists/trixie/pve-no-subscription/binary-amd64/…deb). Existing mirrors are unaffected because tsumugu skips the local debian/dists -> pve/dists symlink left by the old script (and keeps it in the delete pass), but a new deployment has no such symlink and will download a full second copy.

Could you state whether the wider scope is intended and give the expected disk usage vs. the old script? Summing Size: from the Packages indices (deduped by Filename:) gives a close estimate. If not intended, restrict with include/exclude rules; for dists, either create the symlink before running tsumugu or exclude ^/dists/.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wider scope was not intended — the default is now restricted back to the legacy apt-sync coverage in b83cb08: repos pve/pbs/pbs-client/pmg, all suites upstream publishes for them (currently bullseye/bookworm/trixie, the same set as apt-sync.py's @debian-current), amd64 only, plus the top-level files, via ordered --exclusion-v2 rules. PROXMOX_DEB_ALL=1 opts back into the full /debian/ tree. Measured with tsumugu sync --dry-run against live upstream: restricted scope = ~48.5k objects / ~398 GiB apparent; the excluded extras (ceph-*, corosync-3, pdm, devel, arm64 trees) add roughly 90+ GiB. The fresh-install duplicate is fixed by pre-creating debian/dists -> pve/dists before syncing (verified in the dry-run: tsumugu records the symlink in its keep-set and never descends into it). One operational caveat, now also in the script comment: tsumugu treats excluded remote paths as absent, so narrowing scope on an existing full mirror deletes the extra local trees — bounded by --max-delete, which aborts the run when exceeded.

Comment thread proxmox-deb-img.sh Outdated
Comment on lines +3 to +10
WORKDIR="${TUNASYNC_WORKING_DIR:-/data/mirrors/proxmox}"
UPSTREAM="${TUNASYNC_UPSTREAM_URL:-http://download.proxmox.com/}"
MAXDELETE="${TUNASYNC_TSUMUGU_MAXDELETE:-10000}"
THREADS="${TUNASYNC_TSUMUGU_THREADS:-1}"
USERAGENT="${TUNASYNC_TSUMUGU_USERAGENT:-tsumugu}"

tsumugu sync \
--timezone 0 --user-agent "$USERAGENT" --max-delete "$MAXDELETE" \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates tsumugu.sh and drifts from its defaults:

  • WORKDIR defaults to a site-specific /data/mirrors/proxmox; other scripts require TUNASYNC_WORKING_DIR.
  • User agent is bare tsumugu rather than the versioned tsumugu/<ver>; threads default 1 instead of 2; NO_COLOR=1 isn't exported.
  • --timezone 0 is hardcoded.

Could this reuse tsumugu.sh (called twice) or at least its variable handling?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct reuse isn't possible in our deployment: production bind-mounts only this single script into the tsumugu image, so tsumugu.sh isn't present beside it to call. b83cb08 instead inlines its variable handling and fixes the drift you listed: TUNASYNC_WORKING_DIR is now required (no site-specific default), threads default 2, maxdelete default 1000, versioned tsumugu/<ver> user agent, NO_COLOR=1 exported. --timezone 0 stays, now with a comment: it disables tsumugu's recursive-HEAD timezone guessing, which has panicked on other mirrors (zabbix-app), and the upstream listings are UTC.

Comment thread proxmox-deb-img.sh Outdated
tsumugu sync \
--timezone 0 --user-agent "$USERAGENT" --max-delete "$MAXDELETE" \
--parser nginx --threads "$THREADS" \
--ignore-status 503 \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why --ignore-status 503 here? It can mask a genuine upstream outage on /images/. Please add a comment explaining which paths return 503 and why that's safe to ignore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in b83cb08. I probed /images/ today (root, mail/, system/, sample files — all 200) and the flag has no documented origin in our history (it appeared in the job-split commit; the pre-split script didn't have it), so I can't name a path that needs it — keeping it would only mask a genuine outage. If a specific path ever turns out to 503 intermittently we can re-add it scoped, with the comment you asked for.

Comment thread proxmox-deb-img.sh Outdated
Comment on lines +14 to +15
--exclude '^/dists/trixie/pve-test/binary-arm64/' \
--exclude '^/pve/dists/trixie/pve-test/binary-arm64/' \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These trixie pve-test/binary-arm64 excludes are hardcoded to a single suite and will go stale. Please add a comment on why they're needed (and whether a suite-agnostic regex would do).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced by the suite-agnostic --exclude '/binary-arm64/' in b83cb08, with a comment. Background: those trees returned 401 when listed upstream at the time the rules were added (they have since been fixed), and the legacy scope is amd64-only anyway — a generic rule can't go stale when the next Debian suite appears.

Comment thread proxmox-iso.py Outdated
print(f'proxmox iso: metadata probe failed for missing {name}: {e}', file=sys.stderr, flush=True)
errors.append(f'{name}: probe {e}')
continue
if target.exists() and size >= 0 and target.stat().st_size == size:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Size-only comparison. tsumugu and the old lftp --only-newer also compare mtime, so a same-size replacement (e.g. re-signed .asc, re-spun ISO) will never be re-fetched here. Since Last-Modified is already fetched, comparing it against the local mtime would be cheap.

Related: when Content-Length is absent, size is -1 (lines 106/113), so this condition is false and the file is re-downloaded on every run.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b83cb08: up_to_date() now requires every available signal to match — size when Content-Length was sent AND Last-Modified compared against the local mtime when sent — so same-size replacements (re-signed .asc, re-spun ISO) are re-fetched. Your related point is fixed too: when Content-Length is absent, a matching Last-Modified now suffices to skip instead of re-downloading every run; with neither signal the file is still re-downloaded. Covered by integration tests (same-size/newer-mtime refetch, no-Content-Length skip/refetch).

Comment thread proxmox-iso.py

# Delete only stale regular files directly under /iso/. This cannot affect
# /debian/ or /images/ because this stage never traverses outside work.
stale = [p for p in work.iterdir() if p.is_file() and not p.name.startswith('.tmp.') and p.name not in remote_names]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.tmp.* files are excluded from stale cleanup, but if tunasync kills the job mid-download the partial file is left behind. It's only overwritten if the same name reappears, so a partial ISO for a file later removed upstream stays on disk forever. Consider deleting stray .tmp.* at startup (or including them in cleanup).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b83cb08: leftover .tmp.* files under the iso workdir are removed at startup. Downloads always restart from scratch and rename atomically, so this is safe, and a partial file whose remote name later disappears no longer stays on disk forever.

Comment thread proxmox-iso.py Outdated
Comment on lines +72 to +73
name = urllib.parse.unquote(Path(parsed.path).name)
if not name or name in ('.', '..') or name.endswith('/'):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After Path(...).name, name.endswith('/') and name in ('.', '..') can never be true. A link like ./ or /iso/ would become a file named iso, and /iso/sub/x.iso would be flattened to x.iso. Checking parsed.path.endswith('/') and requiring no further / after base_path would make the guard actually work. (The current page has no such links, so this is defensive.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b83cb08: the guard now works on parsed.path — trailing-slash links are rejected before Path().name, and any path still containing / after the base_path prefix (i.e. a subdirectory link) is skipped instead of being flattened. Integration tests cover ./, /iso/ and /iso/sub/x.iso links.

Comment thread proxmox-iso.py Outdated
# been processed, and only if the count is <= TUNASYNC_PROXMOX_ISO_MAXDELETE.
#
# This stage deliberately does not verify SHA256 itself; however the mirror was
# separately reviewed after implementation and all official *.sha256 files

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This note records a one-off manual check on one deployment; it doesn't describe the code's behaviour and will age badly. Suggest either verifying .sha256 in the script or dropping the sentence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with real verification in b83cb08: .sha256 files are fetched first, and every payload this run fetched (or whose .sha256 changed) is hashed and compared. A mismatch removes both sides of the pair (so the next run re-fetches a consistent set) and fails the job. The one-off note is gone.

@yaoge123

Copy link
Copy Markdown
Contributor Author

Round-2 Copilot review addressed in 65f12f8:

  1. Redirects followed without host validation — fixed: open_same_host() now validates resp.geturl()'s host against the upstream-derived allowlist for the page fetch, both metadata probes (HEAD and ranged GET), and downloads; a same-host link redirecting to an arbitrary host is refused.
  2. Dropped REPO_SIZE_FILE/size-sum.sh reporting — fixed: the driver creates REPO_SIZE_FILE via mktemp+trap, appends per-tree du totals (+<bytes> lines, repo convention), and finalizes through helpers/size-sum.sh when available, with a direct numfmt "size-sum:" fallback for minimal images.
  3. ISO stage never invoked — fixed: proxmox-deb-img.sh now runs proxmox-iso.py when it sits beside the driver and python3 is available; split-job deployments and minimal images skip it gracefully.
  4. Trailing-slash links bypassing the directory filter — fixed: parsed.path.endswith('/') is checked before Path().name strips the slash, so /iso/ or ./ links are skipped instead of producing a bogus iso artifact.

Verified end-to-end against a local test server (cross-host redirect link refused and not fetched; /iso/, ./, ../ links skipped) and with a stubbed driver run (ISO stage wired when present, cleanly skipped when absent, size-sum printed both ways).

On the lingering USERAGENT finding from round 1: resolved by construction — both stages now read TUNASYNC_TSUMUGU_USERAGENT with the same default (tsumugu); verified in current branch code (proxmox-deb-img.sh line 7 and proxmox-iso.py line 43). The deb-img and iso stages run as separate tunasync jobs in our deployment, so a shell-side export could not reach the Python process anyway.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread proxmox-deb-img.sh
Comment on lines +1 to +4
#!/bin/bash
set -euo pipefail

_here=$(dirname "$(realpath "$0")")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

proxmox.sh is restored as a forwarding wrapper in b83cb08: it runs each stage the current image supports (deb/images when tsumugu is present, iso when python3 is), prints a loud stderr warning for every skipped stage, and exits non-zero when no stage can run. A single wrapper can't span both images, so the updated PR description documents the two-job deployment (one shared working dir, tunathu/tsumugu + tunathu/tunasync-scripts) with a config example.

Comment thread proxmox-deb-img.sh Outdated
Comment on lines +33 to +35
if [ -f "${_here}/proxmox-iso.py" ] && command -v python3 >/dev/null 2>&1; then
python3 "${_here}/proxmox-iso.py"
fi

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional call is gone in b83cb08: proxmox-deb-img.sh no longer invokes the ISO stage at all — it is a separate job with its own tunasync status, so a broken image fails that job instead of silently publishing without ISOs. The compat proxmox.sh wrapper likewise exits non-zero when no stage can run and warns on stderr for each skipped stage.

Comment thread proxmox-deb-img.sh Outdated
Comment on lines +6 to +7
WORKDIR="${TUNASYNC_WORKING_DIR:-/data/mirrors/proxmox}"
UPSTREAM="${TUNASYNC_UPSTREAM_URL:-http://download.proxmox.com/}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b83cb08, though slightly differently: both scripts now require TUNASYNC_WORKING_DIR with a clear error instead of keeping two fallbacks that could diverge (tunasync's command provider always sets it — worker/cmd_provider.go), so the shell-default/Python-KeyError mismatch is gone. The Python side now exits with a readable message instead of a traceback.

Comment thread proxmox-iso.py Outdated
Comment on lines +55 to +58
final_netloc = urllib.parse.urlparse(resp.geturl()).netloc
if final_netloc != allowed_netloc:
resp.close()
raise ValueError(f'redirect to unexpected host: {final_netloc}')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b83cb08: a redirect target must keep an http(s) scheme, the allowed netloc, AND a path under base_path, otherwise the response is refused before any bytes are consumed. An integration test confirms a redirect from /iso/ to /outside/ is rejected and the file is not saved.

@yaoge123

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — all three points are addressed in b83cb08 and the rewritten PR description:

1. Description vs. diff. The description now matches the diff: the old proxmox.sh implementation is deleted and replaced by proxmox-deb-img.sh + proxmox-iso.py (plus a compat wrapper, see below). The specific misstatements are corrected: /pmg, /pve, /devel are repos under debian/, and /iso/ is handled by the Python scraper precisely because tsumugu cannot parse that page. One factual correction on your side: helpers/size-sum.sh does exist in this repo (155 bytes; used by bazel-apt.sh, chef.sh, cvmfs.sh, …) — but you are right that size reporting is new for this mirror, not "preserved"; the old script reported nothing, and the description now says so.

2. Deleted entry point. proxmox.sh is restored as a forwarding wrapper: it runs each stage whose runtime exists in the current image (tsumugu → deb/images, python3 → iso), prints a loud stderr warning for every skipped stage, and exits non-zero when no stage can run. A single wrapper cannot span both images, so the PR description documents the recommended two-job deployment (one shared workdir, tunathu/tsumugu + tunathu/tunasync-scripts) with a config example.

3. Recursive /iso/. Now stated explicitly in the script header and the description: only the top-level /iso/ listing is mirrored; subdirectory links are skipped, and the scraper must be extended if upstream ever adds subdirectories.

Scope note from the inline thread: the /debian/ default scope is back to the legacy pve/pbs/pbs-client/pmg + amd64 coverage (PROXMOX_DEB_ALL=1 opts into the full tree). Since tsumugu treats excluded remote paths as absent and deletes matching local content (bounded by --max-delete), mirrors that intentionally run full scope should set that env var — or dry-run — before deploying this revision.

Delete the legacy apt-sync/lftp based proxmox.sh and split the mirror
into two stages with different runtime requirements:

- proxmox-deb-img.sh (new): syncs /debian/ and /images/ with tsumugu.
  The default scope matches the legacy script — repos pve, pbs,
  pbs-client, pmg; every suite upstream publishes for them (currently
  bullseye, bookworm, trixie); amd64 only; plus top-level key files.
  PROXMOX_DEB_ALL=1 widens it to the complete /debian/ tree. The
  debian/dists symlink is created before syncing so tsumugu keeps it in
  its keep-set and never downloads the duplicate tree. Size accounting
  follows the repo convention (+<bytes> in REPO_SIZE_FILE, summed by
  helpers/size-sum.sh, which already exists in-tree).

- proxmox-iso.py (new): scrapes the custom top-level /iso/ HTML page,
  accepts only same-host /iso/ links with conservative filenames, and
  validates redirect targets against the same allowlist. Files are
  compared by size and Last-Modified, and freshly downloaded .iso files
  are verified against the upstream .sha256 files. Per-round cleanup
  removes stale .tmp files and vanished artifacts.

- proxmox.sh (kept): backward-compatible wrapper for legacy single-job
  setups. It runs every stage the current image supports (tsumugu ->
  proxmox-deb-img.sh, python3 -> proxmox-iso.py), warns loudly about
  skipped stages, and exits non-zero when no stage can run.

Deployment: two tunasync jobs sharing one working directory, one per
image, each invoking its stage script directly (config example in the
PR description).

Test evidence: tsumugu --dry-run over the default scope matches the
legacy mirror (~48.5k objects, ~398 GiB, debian/dists kept as symlink);
stub-based integration tests for proxmox-iso.py cover 18 cases
(fresh download, size/mtime skip, changed size, changed mtime, sha256
mismatch abort, redirect allowlist, .tmp cleanup, ...); the wrapper was
exercised in 5 cases (both stages, deb-only, iso-only, neither, failure
propagation).
@yaoge123

Copy link
Copy Markdown
Contributor Author

This branch has been squashed into a single commit for merge-readiness: 6684841 ("Rewrite proxmox.sh: replace apt-sync+lftp with tsumugu+scraper"). The earlier commits referenced throughout this thread (2ea1ec0, 867ae01, 65f12f8, b83cb08) are now orphaned — still viewable at their SHAs, but the current diff is the only thing being merged. Content is byte-identical to b83cb08 (empty git diff, identical tree hash 38d3177); this was a pure history rewrite.

Review items → resolution (all verified against the final code at 6684841):

@happyaron's review body:

  1. PR description doesn't match the diff — description fully rewritten: three-file roles, pmg/pve/devel under debian/, size reporting correctly described as new (and helpers/size-sum.sh does exist in-repo, used by several other scripts).
  2. Deleting proxmox.sh breaks existing deployments / two-job setup undocumented — proxmox.sh kept as a compatibility wrapper (runs whichever stages the image provides, non-zero exit when none can); two-job tunasync config example added to the description.
  3. /iso no longer recursive — stated explicitly in the script header and the description: top-level listing only, subdirectory links skipped.

@happyaron's inline comments:

  1. Scope increase + debian/dists duplication on fresh installs — default scope restricted back to legacy coverage (pve/pbs/pbs-client/pmg, amd64, current suites); debian/dists -> pve/dists symlink created before syncing so fresh installs never fetch the duplicate tree; PROXMOX_DEB_ALL=1 opts into the full tree.
  2. Duplicates tsumugu.sh / drifts from its defaults — direct reuse is impossible (production bind-mounts only this one script into the image); the inlined variables now follow tsumugu.sh defaults exactly (required TUNASYNC_WORKING_DIR, threads 2, maxdelete 1000, versioned user agent, NO_COLOR), with a comment saying so.
  3. Unexplained --ignore-status 503 on /images/ — option removed entirely after probing /images/ (root, mail/, system/, sample files all return 200).
  4. Hardcoded trixie pve-test/binary-arm64 excludes go stale — replaced by the suite-agnostic --exclude '/binary-arm64/' with an explanatory comment.
  5. Size-only comparison; missing Content-Length re-downloads every run — up_to_date() now requires every available signal to match (size AND Last-Modified vs local mtime); with neither signal the file is re-downloaded.
  6. Stray .tmp.* left behind by killed jobs — leftover .tmp.* files are removed at startup, before any work.
  7. Dead guard after Path(...).name — the guard now operates on parsed.path before name flattening, so trailing-slash and subdirectory links are actually skipped.
  8. One-off manual .sha256 check note would age badly — replaced by real verification: .sha256 files are fetched first and every downloaded payload is checked; a mismatch removes both sides of the pair and fails the run.

Copilot (final round):

  1. Entry point deleted with no wrapper/config — proxmox.sh restored as a forwarding wrapper.
  2. Missing python3 silently skipped, size still reported — the conditional call is gone from proxmox-deb-img.sh; the wrapper warns per skipped stage and exits non-zero when no stage can run.
  3. Shell WORKDIR default never exported, Python KeyError — both scripts now require TUNASYNC_WORKING_DIR (tunasync sets it for every job); no defaults.
  4. Redirect check compared only netloc — a redirect target must keep an http(s) scheme, the upstream-derived host, and a path under base_path, or it is refused.

Test evidence on the final code: tsumugu sync --dry-run over the default scope matches the legacy mirror (~48.5k objects, ~398 GiB, debian/dists kept as symlink); stub-based integration tests for proxmox-iso.py (18 cases); the wrapper exercised in 5 cases. Both stages have been running in production on mirror.nju.edu.cn.

Some earlier in-thread replies described intermediate states; the current diff and this summary are authoritative.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants