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")
|
||||
Reference in New Issue
Block a user