1- import requests
2- import sys
3- import os
4- import zipfile
5- import io
6- import platform
1+ # python_v2ray/downloader.py
2+
3+ import requests , sys , os , zipfile , io , platform
74from pathlib import Path
85from typing import Optional
96
107XRAY_REPO = "GFW-knocker/Xray-core"
118OWN_REPO = "arshiacomplus/python_v2ray"
9+ HYSTERIA_REPO = "apernet/hysteria"
1210
1311class BinaryDownloader :
14- """
15- * Handles the logic of downloading and extracting necessary binaries
16- * from GitHub Releases for the current operating system and architecture.
17- """
1812 def __init__ (self , project_root : Path ):
1913 self .project_root = project_root
2014 self .vendor_path = self .project_root / "vendor"
@@ -23,52 +17,54 @@ def __init__(self, project_root: Path):
2317 self .arch = self ._get_arch_name ()
2418
2519 def _get_os_name (self ) -> str :
26- """* Determines the OS name used in release assets."""
2720 if sys .platform == "win32" : return "windows"
28- if sys .platform == "darwin" : return "macos "
21+ if sys .platform == "darwin" : return "darwin "
2922 return "linux"
3023
3124 def _get_arch_name (self ) -> str :
32- """* Determines the architecture name (e.g., 64, 32, arm64-v8a)."""
3325 machine = platform .machine ().lower ()
34- if "amd64" in machine or "x86_64" in machine :
35- return "64"
36- if "arm64" in machine or "aarch64" in machine :
37- return "arm64-v8a"
38- if "386" in machine or "x86" in machine :
39- return "32"
26+ if "amd64" in machine or "x86_64" in machine : return "amd64"
27+ if "arm64" in machine or "aarch64" in machine : return "arm64"
28+ if "386" in machine or "x86" in machine : return "386"
4029 return "unsupported"
4130
4231 def _get_asset_url (self , assets : list , name_prefix : str ) -> Optional [str ]:
43- """
44- * It finds the download URL for a specific asset based on OS and arch.
45- """
46- asset_name = f"{ name_prefix } -{ self .os_name } -{ self .arch } .zip"
47- print (f"note: Searching for asset: { asset_name } " )
32+ # * This logic is now smarter to handle different naming conventions
33+
34+ if name_prefix == "hysteria" :
35+ asset_name = f"{ name_prefix } -{ self .os_name } -{ self .arch } "
36+ if self .os_name == "windows" :
37+ asset_name += ".exe"
38+ elif name_prefix == "Xray" :
39+ arch_name = "64" if self .arch == "amd64" else self .arch
40+ os_name = "macos" if self .os_name == "darwin" else self .os_name # Xray uses macos
41+ asset_name = f"{ name_prefix } -{ os_name } -{ arch_name } .zip"
42+ else :
43+ arch_name = "64" if self .arch == "amd64" else self .arch
44+ os_name = "macos" if self .os_name == "darwin" else self .os_name
45+ asset_name = f"{ name_prefix } -{ os_name } -{ arch_name } .zip"
4846
47+ print (f"note: Searching for asset: { asset_name } " )
4948 for asset in assets :
5049 if asset ['name' ].lower () == asset_name .lower ():
5150 return asset ['browser_download_url' ]
5251 return None
5352
5453 def ensure_binary (self , name : str , target_dir : Path , repo : str ) -> bool :
55- """
56- * Checks for a binary, and if not found, downloads and extracts the
57- * entire contents of the zip file (including .dat files).
58- """
5954 exe_name = f"{ name } .exe" if sys .platform == "win32" else name
6055 target_file = target_dir / exe_name
6156
62- geoip_file = target_dir / "geoip.dat"
63-
64- if target_file .is_file () and (name != "xray" or geoip_file .is_file ()):
65- print (f"* Binary '{ exe_name } ' and necessary assets already exist." )
57+ if target_file .is_file ():
58+ print (f"* Binary '{ exe_name } ' already exists." )
6659 return True
6760
68- print (f"! Binary '{ exe_name } ' not found. Attempting to download from '{ repo } '..." )
69-
61+ print (f"! Binary '{ exe_name } ' not found. Downloading from '{ repo } '..." )
7062 try :
71- release_url = f"https://api.github.com/repos/{ repo } /releases/latest"
63+ if name == "hysteria" :
64+ release_url = f"https://api.github.com/repos/{ repo } /releases/tags/app%2Fv2.6.2" # ! Hardcoded to a specific stable version
65+ else :
66+ release_url = f"https://api.github.com/repos/{ repo } /releases/latest"
67+
7268 response = requests .get (release_url , timeout = 10 )
7369 response .raise_for_status ()
7470 assets = response .json ().get ("assets" , [])
@@ -77,36 +73,46 @@ def ensure_binary(self, name: str, target_dir: Path, repo: str) -> bool:
7773 download_url = self ._get_asset_url (assets , asset_prefix )
7874
7975 if not download_url :
80- print (f"! ERROR: Could not find downloadable asset for '{ name } ' matching ' { self . os_name } - { self . arch } ' in repo ' { repo } ' ." )
76+ print (f"! ERROR: Could not find downloadable asset for '{ name } '." )
8177 return False
8278
83- print (f"* Downloading from : { download_url } " )
79+ print (f"* Downloading: { download_url } " )
8480 asset_response = requests .get (download_url , timeout = 120 , stream = True )
8581 asset_response .raise_for_status ()
8682
87- print (f"* Extracting all files to '{ target_dir } '..." )
88- with zipfile .ZipFile (io .BytesIO (asset_response .content )) as z :
89- z .extractall (path = target_dir )
90- print (f"* Successfully downloaded and extracted all assets for '{ name } '." )
91-
92- if sys .platform != "win32" and target_file .is_file ():
83+ if not download_url .endswith ('.zip' ):
84+ with open (target_file , "wb" ) as f :
85+ f .write (asset_response .content )
86+ else :
87+ with zipfile .ZipFile (io .BytesIO (asset_response .content )) as z :
88+ for member_name in z .namelist ():
89+ if Path (member_name ).name .lower () == exe_name .lower ():
90+ source = z .open (member_name )
91+ target = open (target_file , "wb" )
92+ with source , target : target .write (source .read ())
93+
94+ for dat_file in ["geoip.dat" , "geosite.dat" ]:
95+ if dat_file in z .namelist ():
96+ with z .open (dat_file ) as source_dat , open (target_dir / dat_file , "wb" ) as target_dat :
97+ target_dat .write (source_dat .read ())
98+
99+ print (f"* Successfully downloaded '{ exe_name } '." )
100+ if sys .platform != "win32" :
93101 os .chmod (target_file , 0o755 )
94-
95102 return True
96103
97104 except Exception as e :
98- print (f"! ERROR during download/extraction: { e } " )
105+ print (f"! ERROR during download/extraction for ' { name } ' : { e } " )
99106 return False
100107
101108 def ensure_all (self ):
102- """* Ensures all necessary binaries are present."""
103109 print ("--- Checking for necessary binaries & databases ---" )
104110 self .vendor_path .mkdir (exist_ok = True )
105111 self .core_engine_path .mkdir (exist_ok = True )
106-
107112 xray_ok = self .ensure_binary ("xray" , self .vendor_path , XRAY_REPO )
108113 engine_ok = self .ensure_binary ("core_engine" , self .core_engine_path , OWN_REPO )
114+ hysteria_ok = self .ensure_binary ("hysteria" , self .vendor_path , HYSTERIA_REPO )
109115
110116 print ("-------------------------------------------------" )
111- if not (xray_ok and engine_ok ):
112- raise RuntimeError ("Could not obtain all necessary files ." )
117+ if not (xray_ok and engine_ok and hysteria_ok ):
118+ raise RuntimeError ("Could not obtain all necessary binaries ." )
0 commit comments