About Multi-camera viewer optimized for RTSP streams
1#!/usr/bin/env python3
2"""Prepare GitHub release assets without exceeding the per-asset size limit."""
3
4from __future__ import annotations
5
6import argparse
7import glob
8import hashlib
9from pathlib import Path
10import shutil
11
12
13DEFAULT_MAX_ASSET_SIZE = 1900 * 1024 * 1024
14BUFFER_SIZE = 8 * 1024 * 1024
15
16
17def file_sha256(path: Path) -> str:
18 digest = hashlib.sha256()
19 with path.open("rb") as handle:
20 for chunk in iter(lambda: handle.read(BUFFER_SIZE), b""):
21 digest.update(chunk)
22 return digest.hexdigest()
23
24
25def split_file(source: Path, release_dir: Path, max_asset_size: int) -> list[Path]:
26 parts: list[Path] = []
27 part_number = 1
28
29 with source.open("rb") as handle:
30 while True:
31 target = release_dir / f"{source.name}.part{part_number:02d}"
32 written = 0
33
34 with target.open("wb") as output:
35 while written < max_asset_size:
36 chunk = handle.read(min(BUFFER_SIZE, max_asset_size - written))
37 if not chunk:
38 break
39 output.write(chunk)
40 written += len(chunk)
41
42 if written == 0:
43 target.unlink(missing_ok=True)
44 break
45
46 parts.append(target)
47 part_number += 1
48
49 return parts
50
51
52def find_sources(platform: str, include_appimage: bool) -> list[Path]:
53 patterns = [f"dist/*_{platform}_*.zip"]
54 if include_appimage:
55 patterns.append(f"dist/*_{platform}_*.AppImage")
56
57 return [Path(path) for pattern in patterns for path in glob.glob(pattern)]
58
59
60def prepare_release_assets(platform: str, include_appimage: bool, max_asset_size: int) -> None:
61 release_dir = Path("dist") / "release-assets"
62 release_dir.mkdir(parents=True, exist_ok=True)
63
64 sources = find_sources(platform, include_appimage)
65 if not sources:
66 raise SystemExit("No release assets found")
67
68 checksums: list[str] = []
69 split_assets: list[str] = []
70
71 for source in sources:
72 if source.stat().st_size < max_asset_size:
73 target = release_dir / source.name
74 shutil.copy2(source, target)
75 checksums.append(f"{file_sha256(target)} {target.name}")
76 continue
77
78 split_assets.append(source.name)
79 for part in split_file(source, release_dir, max_asset_size):
80 checksums.append(f"{file_sha256(part)} {part.name}")
81
82 checksum_path = release_dir / f"wildcam_{platform}_SHA256SUMS.txt"
83 checksum_path.write_text("\n".join(checksums) + "\n", encoding="utf-8")
84
85 if split_assets:
86 note_path = release_dir / f"wildcam_{platform}_split_assets.txt"
87 note_path.write_text(
88 "Some release assets exceeded GitHub's 2 GiB per-file limit and were split.\n"
89 "Reassemble a split asset with: cat <asset>.part* > <asset>\n"
90 "Split assets:\n"
91 + "\n".join(f"- {asset}" for asset in split_assets)
92 + "\n",
93 encoding="utf-8",
94 )
95
96
97def parse_args() -> argparse.Namespace:
98 parser = argparse.ArgumentParser()
99 parser.add_argument("--platform", required=True)
100 parser.add_argument("--include-appimage", action="store_true")
101 parser.add_argument("--max-asset-size", type=int, default=DEFAULT_MAX_ASSET_SIZE)
102 return parser.parse_args()
103
104
105def main() -> None:
106 args = parse_args()
107 prepare_release_assets(args.platform, args.include_appimage, args.max_asset_size)
108
109
110if __name__ == "__main__":
111 main()