Initial release: full Arma 3 mod management toolchain
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>
This commit is contained in:
168
update_mods.py
Normal file
168
update_mods.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user