@@ -37,17 +37,19 @@ def __getattr__(self, name):
3737
3838
3939class Client :
40- def __init__ (self , instance_url : str | UrlBuilder ):
41- self . urls = (
42- instance_url
43- if isinstance ( instance_url , UrlBuilder )
44- else UrlBuilder ( instance_url )
45- )
40+ def __init__ (self , urls : UrlBuilder ):
41+ """
42+ Initialize with a UrlBuilder instance.
43+ Uses backend_url for API calls and frontend_url for headers.
44+ """
45+ self . urls = urls
4646 self ._session = requests .Session ()
47+
48+ # Set headers based on the frontend URL to avoid CORS/Security issues
4749 self ._session .headers .update (
4850 {
49- "Origin" : self .urls .instance_url ,
50- "Referer" : self .urls .instance_url ,
51+ "Origin" : self .urls .frontend_url . rstrip ( "/" ) ,
52+ "Referer" : self .urls .frontend_url ,
5153 }
5254 )
5355
@@ -61,11 +63,18 @@ def close(self):
6163 self ._session .close ()
6264
6365 @classmethod
64- def resolve (cls , instance_url : str | None = None ) -> "Client" :
65- """Resolve instance URL from arg/env/prompt and return Client."""
66- urls = UrlBuilder .resolve (instance_url )
66+ def resolve (cls , initial_url : str | None = None ) -> "Client" :
67+ """Resolve URLs via UrlBuilder and return a Client."""
68+ urls = UrlBuilder .resolve (initial_url )
6769 return cls (urls )
6870
71+ def get_config (self ) -> dict :
72+ """Fetch the server configuration using the backend URL."""
73+ url = self .urls .config_url ()
74+ response = self ._session .get (url , timeout = 30 )
75+ response .raise_for_status ()
76+ return response .json ()
77+
6978 def upload_file (
7079 self ,
7180 file_path : Path ,
@@ -74,21 +83,18 @@ def upload_file(
7483 expire_after : int | None = None ,
7584 ) -> dict :
7685 """
77- Stream-upload *file_path* to ``POST /upload`` on the backend.
78- Returns the JSON response (contains the download key) .
86+ Stream-upload *file_path* to the backend.
87+ Uses the resolved backend_url/upload endpoint .
7988 """
80- if expire_after_n_download is None :
81- expire_after_n_download = settings .EXPIRE_AFTER_N_DOWNLOAD
82-
83- if expire_after is None :
84- expire_after = settings .EXPIRE_AFTER
89+ # Resolve expiration settings
90+ expire_n = expire_after_n_download or settings .EXPIRE_AFTER_N_DOWNLOAD
91+ expire_t = expire_after or settings .EXPIRE_AFTER
8592
86- if expire_after_n_download is None or expire_after is None :
93+ # Fallback to server defaults if not set locally
94+ if expire_n is None or expire_t is None :
8795 config = self .get_config ()
88- if expire_after_n_download is None :
89- expire_after_n_download = config ["default_number_of_downloads" ]
90- if expire_after is None :
91- expire_after = config ["default_expiry" ]
96+ expire_n = expire_n or config .get ("default_number_of_downloads" )
97+ expire_t = expire_t or config .get ("default_expiry" )
9298
9399 upload_url = self .urls .upload_url ()
94100 display_name = filename or file_path .name
@@ -106,41 +112,52 @@ def upload_file(
106112 ):
107113 wrapped = _ProgressReader (f , pbar )
108114 wrapped_io = cast (BinaryIO , wrapped )
115+
116+ # Multipart form data
109117 files = {"file" : (display_name , wrapped_io , "application/octet-stream" )}
110118 data = {
111119 "filename" : display_name ,
112- "expire_after_n_download" : str (expire_after_n_download ),
113- "expire_after" : str (expire_after ),
120+ "expire_after_n_download" : str (expire_n ),
121+ "expire_after" : str (expire_t ),
114122 }
115123
116124 response = self ._session .post (
117125 upload_url ,
118126 data = data ,
119127 files = files ,
120- timeout = (30 , None ),
128+ timeout = (
129+ 30 ,
130+ None ,
131+ ), # 30s connect timeout, infinite read timeout for large files
121132 )
133+
134+ # If the backend returned HTML (redirect/error), catch it here
135+ if "text/html" in response .headers .get ("Content-Type" , "" ):
136+ raise ConnectionError (
137+ f"Upload failed: Server returned HTML instead of JSON from { upload_url } "
138+ )
139+
122140 response .raise_for_status ()
123141 return response .json ()
124142
125- def get_config (self ) -> dict :
126- """Fetch the server configuration."""
127- url = self .urls .config_url ()
128- response = self ._session .get (url , timeout = 30 )
129- response .raise_for_status ()
130- return response .json ()
131-
132143 def download_to_file (self , key : str , dest : Path ) -> Path :
133- """
134- Stream-download a file and write it to *dest* with a progress bar.
135- Returns *dest*.
136- """
144+ """Stream-download a file using the backend URL."""
137145 download_url = f"{ self .urls .download_url ()} { key } "
138146
139147 with self ._session .get (
140148 download_url , stream = True , timeout = (30 , None )
141149 ) as response :
150+ # Validation: Catch frontend HTML responses before they hit the crypto layer
151+ content_type = response .headers .get ("Content-Type" , "" )
152+ if "text/html" in content_type :
153+ raise ConnectionError (
154+ f"Expected binary file, but got HTML. Your API URL is likely wrong.\n "
155+ f"Attempted URL: { download_url } "
156+ )
157+
142158 response .raise_for_status ()
143159 total = int (response .headers .get ("content-length" , 0 )) or None
160+
144161 with (
145162 open (dest , "wb" ) as f ,
146163 tqdm (
0 commit comments