initial commit
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
"""Download a release archive and extract it. A complete local file is extracted without downloading again."""
|
||||
import tarfile
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
UA = "LLLL-tools"
|
||||
|
||||
|
||||
def _archive_name(path: Path) -> str:
|
||||
name = path.name.lower()
|
||||
if name.endswith(".part"):
|
||||
name = name[: -len(".part")]
|
||||
return name
|
||||
|
||||
|
||||
def archive_complete(path: Path) -> bool:
|
||||
if not path.is_file() or path.stat().st_size == 0:
|
||||
return False
|
||||
name = _archive_name(path)
|
||||
try:
|
||||
if name.endswith(".zip") or name.endswith(".ipa"):
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
return bool(archive.infolist())
|
||||
if ".tar." in name or name.endswith(".tgz"):
|
||||
mode = "r:xz" if name.endswith(".tar.xz") else "r:gz"
|
||||
with tarfile.open(path, mode) as archive:
|
||||
for _ in archive:
|
||||
pass
|
||||
return True
|
||||
except (OSError, tarfile.TarError, zipfile.BadZipFile):
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def _total_size(resp, have: int, code: int) -> int | None:
|
||||
if code == 206:
|
||||
content_range = resp.headers.get("Content-Range")
|
||||
if content_range and "/" in content_range:
|
||||
total = content_range.rsplit("/", 1)[-1]
|
||||
if total.isdigit():
|
||||
return int(total)
|
||||
length = resp.headers.get("Content-Length")
|
||||
if length and length.isdigit():
|
||||
return have + int(length)
|
||||
return None
|
||||
length = resp.headers.get("Content-Length")
|
||||
if length and length.isdigit():
|
||||
return int(length)
|
||||
return None
|
||||
|
||||
|
||||
def ensure_archive(url: str, dest: Path) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
part = dest.with_name(dest.name + ".part")
|
||||
if archive_complete(dest):
|
||||
return
|
||||
if archive_complete(part):
|
||||
part.replace(dest)
|
||||
return
|
||||
if dest.exists() and not part.exists():
|
||||
dest.replace(part)
|
||||
elif dest.exists():
|
||||
dest.unlink()
|
||||
|
||||
have = part.stat().st_size if part.exists() else 0
|
||||
headers = {"User-Agent": UA}
|
||||
if have:
|
||||
headers["Range"] = f"bytes={have}-"
|
||||
request = urllib.request.Request(url, headers=headers)
|
||||
try:
|
||||
response = urllib.request.urlopen(request, timeout=120)
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code == 416 and archive_complete(part):
|
||||
part.replace(dest)
|
||||
return
|
||||
print(f"下载失败:{exc.code}")
|
||||
raise SystemExit(2)
|
||||
except (urllib.error.URLError, TimeoutError, OSError):
|
||||
print("下载未完成")
|
||||
raise SystemExit(2)
|
||||
|
||||
try:
|
||||
with response:
|
||||
code = getattr(response, "status", 200)
|
||||
if code == 200:
|
||||
have = 0
|
||||
elif code != 206:
|
||||
print(f"下载失败:{code}")
|
||||
raise SystemExit(2)
|
||||
total = _total_size(response, have, code)
|
||||
shown = -1
|
||||
with part.open("wb" if code == 200 else "ab") as output:
|
||||
done = have if code == 206 else 0
|
||||
while True:
|
||||
chunk = response.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
output.write(chunk)
|
||||
done += len(chunk)
|
||||
if not total:
|
||||
continue
|
||||
pct = min(100, done * 100 // total)
|
||||
if pct // 10 != shown:
|
||||
shown = pct // 10
|
||||
print(f"\r{pct}%", end="", flush=True)
|
||||
if shown >= 0:
|
||||
print()
|
||||
except (urllib.error.URLError, TimeoutError, OSError):
|
||||
print("下载未完成")
|
||||
raise SystemExit(2)
|
||||
|
||||
if archive_complete(part):
|
||||
part.replace(dest)
|
||||
return
|
||||
if total and part.exists() and part.stat().st_size == total:
|
||||
part.unlink(missing_ok=True)
|
||||
print("下载内容无法解压")
|
||||
raise SystemExit(2)
|
||||
print("下载未完成")
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
def extract_archive(archive: Path, dest: Path) -> None:
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
root = dest.resolve()
|
||||
name = archive.name.lower()
|
||||
if name.endswith(".zip"):
|
||||
with zipfile.ZipFile(archive) as zipped:
|
||||
for info in zipped.infolist():
|
||||
target = (dest / info.filename).resolve()
|
||||
if not target.is_relative_to(root):
|
||||
raise SystemExit("压缩包路径不安全")
|
||||
zipped.extractall(dest)
|
||||
return
|
||||
with tarfile.open(archive) as packed:
|
||||
packed.extractall(dest, filter="data")
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Download and extract Zig for this operating system."""
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
from fetch import ensure_archive, extract_archive
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TOOLS = ROOT / "tools"
|
||||
VERSION = "0.16.0"
|
||||
RELEASE = f"https://github.com/ziglang/zig/releases/tag/{VERSION}"
|
||||
|
||||
ARCH = {
|
||||
"amd64": "x86_64",
|
||||
"x86_64": "x86_64",
|
||||
"arm64": "aarch64",
|
||||
"aarch64": "aarch64",
|
||||
}
|
||||
OS_FILE = {
|
||||
"windows": ("windows", "zip", "zig.exe"),
|
||||
"darwin": ("macos", "tar.xz", "zig"),
|
||||
"linux": ("linux", "tar.xz", "zig"),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
system = platform.system().lower()
|
||||
arch = ARCH.get(platform.machine().lower())
|
||||
spec = OS_FILE.get(system)
|
||||
if not arch or not spec:
|
||||
print(RELEASE)
|
||||
raise SystemExit(2)
|
||||
|
||||
os_name, ext, binary = spec
|
||||
folder = f"zig-{arch}-{os_name}-{VERSION}"
|
||||
if any(TOOLS.glob(f"zig*/{binary}")):
|
||||
return
|
||||
url = f"https://ziglang.org/download/{VERSION}/{folder}.{ext}"
|
||||
archive = TOOLS / f"{folder}.{ext}"
|
||||
print(url, flush=True)
|
||||
ensure_archive(url, archive)
|
||||
extract_archive(archive, TOOLS)
|
||||
found = sorted(TOOLS.glob(f"zig*/{binary}"))
|
||||
if not found:
|
||||
raise SystemExit(2)
|
||||
if os.name != "nt":
|
||||
for path in found:
|
||||
path.chmod(0o755)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Download and extract zsign for this operating system."""
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
from fetch import ensure_archive, extract_archive
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TOOLS = ROOT / "tools" / "zsign"
|
||||
VERSION = "1.1.2"
|
||||
RELEASE = f"https://github.com/zhlynn/zsign/releases/tag/v{VERSION}"
|
||||
|
||||
ARCH = {
|
||||
"amd64": "x86_64",
|
||||
"x86_64": "x86_64",
|
||||
"arm64": "aarch64",
|
||||
"aarch64": "aarch64",
|
||||
}
|
||||
# Official v1.1.2 assets. There is no Windows arm64 or macOS x64 build.
|
||||
ASSETS = {
|
||||
("windows", "x86_64"): ("zsign-windows-x64.zip", "zsign.exe"),
|
||||
("darwin", "aarch64"): ("zsign-macos-arm64.tar.gz", "zsign"),
|
||||
("linux", "x86_64"): ("zsign-linux-x86_64.tar.gz", "zsign"),
|
||||
("linux", "aarch64"): ("zsign-linux-aarch64.tar.gz", "zsign"),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
system = platform.system().lower()
|
||||
arch = ARCH.get(platform.machine().lower())
|
||||
asset = ASSETS.get((system, arch))
|
||||
if not asset:
|
||||
print(RELEASE)
|
||||
raise SystemExit(2)
|
||||
|
||||
filename, binary = asset
|
||||
if (TOOLS / binary).is_file():
|
||||
return
|
||||
url = f"https://github.com/zhlynn/zsign/releases/download/v{VERSION}/{filename}"
|
||||
archive = TOOLS.parent / filename
|
||||
print(url, flush=True)
|
||||
ensure_archive(url, archive)
|
||||
extract_archive(archive, TOOLS)
|
||||
installed = TOOLS / binary
|
||||
if not installed.is_file():
|
||||
raise SystemExit(2)
|
||||
if os.name != "nt":
|
||||
installed.chmod(0o755)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user