Pipeline: parse HTML presets, compare modlists, download from Caddy file server, create junctions/symlinks to Arma 3 Server directory. Includes update/sync flows, missing-mod reporting, OS compat layer, shared config, dep checker, comprehensive test suite (71 tests). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
6.0 KiB
Python
169 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
CLI entry point: re-download mod files that have changed on the server.
|
|
|
|
Use this after you have updated mod files on the Caddy server without
|
|
changing the modlist structure (same mods, same Steam IDs, new file versions).
|
|
|
|
Detection method: compare local file sizes against server file sizes.
|
|
A file is considered stale when it is missing locally OR its local size
|
|
differs from the server-reported size. Use --force to ignore size checks
|
|
and re-download every file unconditionally.
|
|
|
|
Usage:
|
|
python update_mods.py # check all groups/mods
|
|
python update_mods.py --group shared # limit to one group
|
|
python update_mods.py --mod @ace # limit to one mod folder
|
|
python update_mods.py --force # re-download everything
|
|
python update_mods.py --force --group shared # force-update one group
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
from tqdm import tqdm
|
|
|
|
from arma_modlist_tools.compat import fix_console_encoding
|
|
from arma_modlist_tools.config import load_config
|
|
from arma_modlist_tools.fetcher import (
|
|
build_server_index, find_mod_folder,
|
|
list_mod_files, list_mod_updates,
|
|
download_file, make_session,
|
|
)
|
|
|
|
fix_console_encoding()
|
|
|
|
|
|
def _fmt_bytes(n: int) -> str:
|
|
for unit in ("B", "KB", "MB", "GB"):
|
|
if n < 1024:
|
|
return f"{n:.1f} {unit}"
|
|
n /= 1024
|
|
return f"{n:.1f} TB"
|
|
|
|
|
|
def _collect_targets(cfg, group_filter: str | None, mod_filter: str | None) -> list[tuple[str, str, object]]:
|
|
"""
|
|
Walk downloads/ and return (group, folder_name, folder_path) for each
|
|
@Mod folder that passes the group/mod filters.
|
|
"""
|
|
targets = []
|
|
if not cfg.downloads.is_dir():
|
|
return targets
|
|
for group_dir in sorted(cfg.downloads.iterdir()):
|
|
if not group_dir.is_dir():
|
|
continue
|
|
if group_filter and group_dir.name != group_filter:
|
|
continue
|
|
for mod_dir in sorted(group_dir.iterdir()):
|
|
if not mod_dir.is_dir() or not mod_dir.name.startswith("@"):
|
|
continue
|
|
if mod_filter and mod_dir.name != mod_filter:
|
|
continue
|
|
targets.append((group_dir.name, mod_dir.name, mod_dir))
|
|
return targets
|
|
|
|
|
|
def main() -> None:
|
|
cfg = load_config()
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Re-download mod files that have changed on the server."
|
|
)
|
|
parser.add_argument("--group", "-g", metavar="GROUP",
|
|
help="Only check mods in this group folder (e.g. shared)")
|
|
parser.add_argument("--mod", "-m", metavar="MOD",
|
|
help="Only check this mod folder name (e.g. @ace)")
|
|
parser.add_argument("--force", action="store_true",
|
|
help="Re-download all files regardless of size match")
|
|
args = parser.parse_args()
|
|
|
|
# ---- Collect local mod folders to check ----
|
|
targets = _collect_targets(cfg, args.group, args.mod)
|
|
if not targets:
|
|
print("\nNo mod folders found matching the given filters.\n")
|
|
sys.exit(0)
|
|
|
|
# ---- Build server index ----
|
|
print(f"\nBuilding server index...")
|
|
index = build_server_index(cfg.server_url, cfg.server_auth)
|
|
print(f" {len(index['by_steam_id'])} mods indexed\n")
|
|
|
|
session = make_session(cfg.server_auth)
|
|
|
|
mode = "force" if args.force else "size-check"
|
|
print(f" Mode: {mode}")
|
|
print(f" Checking {len(targets)} mod folder(s)...\n")
|
|
|
|
# Column widths for the summary table
|
|
COL_MOD = 44
|
|
COL_GROUP = 24
|
|
|
|
total_checked = total_updated = total_bytes = 0
|
|
not_on_server = []
|
|
|
|
for group, folder_name, mod_dir in targets:
|
|
# Find this mod on the server by name (no steam_id available from local dir)
|
|
mod_stub = {"name": folder_name, "steam_id": None}
|
|
folder_url = find_mod_folder(mod_stub, index)
|
|
|
|
if not folder_url:
|
|
tqdm.write(f" [?] {folder_name:<{COL_MOD}} {group:<{COL_GROUP}} not found on server")
|
|
not_on_server.append(f"{group}/{folder_name}")
|
|
continue
|
|
|
|
# Determine which files need downloading
|
|
if args.force:
|
|
stale = list_mod_files(folder_url, session)
|
|
else:
|
|
stale = list_mod_updates(folder_url, mod_dir, session)
|
|
|
|
all_files = list_mod_files(folder_url, session) if not args.force else stale
|
|
checked = len(all_files) if not args.force else len(stale)
|
|
|
|
if not stale:
|
|
print(f" [=] {folder_name:<{COL_MOD}} {group:<{COL_GROUP}} {checked} files up-to-date")
|
|
total_checked += checked
|
|
continue
|
|
|
|
# Download stale files
|
|
mod_bytes = 0
|
|
with tqdm(
|
|
total=len(stale), unit="file",
|
|
desc=f" {folder_name[-COL_MOD:]:<{COL_MOD}}",
|
|
position=0, leave=True, dynamic_ncols=True,
|
|
) as file_bar:
|
|
for rel, url, size in stale:
|
|
dest_file = mod_dir / rel
|
|
with tqdm(
|
|
total=size if size else None,
|
|
unit="B", unit_scale=True, unit_divisor=1024,
|
|
desc=f" {rel[-40:]:40s}",
|
|
position=1, leave=False, dynamic_ncols=True,
|
|
) as chunk_bar:
|
|
n = download_file(url, dest_file, session,
|
|
on_chunk=lambda b: chunk_bar.update(b))
|
|
mod_bytes += n
|
|
file_bar.update(1)
|
|
|
|
total_checked += checked
|
|
total_updated += len(stale)
|
|
total_bytes += mod_bytes
|
|
print(f" [+] {folder_name:<{COL_MOD}} {group:<{COL_GROUP}} "
|
|
f"{checked} files {len(stale)} updated ({_fmt_bytes(mod_bytes)})")
|
|
|
|
print(f"\n{'='*56}")
|
|
print(f" Total: {total_checked} files checked, "
|
|
f"{total_updated} updated, "
|
|
f"{_fmt_bytes(total_bytes)} downloaded")
|
|
if not_on_server:
|
|
print(f" Not found on server ({len(not_on_server)}): {', '.join(not_on_server)}")
|
|
print(f"{'='*56}\n")
|
|
|
|
if total_updated == 0 and not not_on_server:
|
|
print(" All mods are up-to-date.\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|