Skip to content

Commit 2cb48d9

Browse files
committed
release: v0.4.3
1 parent 68fa166 commit 2cb48d9

14 files changed

Lines changed: 410 additions & 53 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "olah"
7-
version = "0.4.2"
7+
version = "0.4.3"
88
description = "Self-hosted lightweight huggingface mirror."
99
readme = "README.md"
1010
requires-python = ">=3.8"

src/olah/configs.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,9 @@ def __init__(self, path: Optional[str] = None) -> None:
9292
self.hf_netloc: str = "huggingface.co"
9393
self.hf_lfs_netloc: str = "cdn-lfs.huggingface.co"
9494

95-
self.mirror_scheme: str = "http" if self.ssl_key is None else "https"
96-
self.mirror_netloc: str = (
97-
f"{self.host if self._is_specific_addr(self.host) else 'localhost'}:{self.port}"
98-
)
99-
self.mirror_lfs_netloc: str = (
100-
f"{self.host if self._is_specific_addr(self.host) else 'localhost'}:{self.port}"
101-
)
95+
self.mirror_scheme: str = self.default_mirror_scheme()
96+
self.mirror_netloc: str = self.default_mirror_netloc()
97+
self.mirror_lfs_netloc: str = self.default_mirror_netloc()
10298

10399
self.mirrors_path: List[str] = []
104100

@@ -116,6 +112,20 @@ def _is_specific_addr(self, host: Union[List[str], str]) -> bool:
116112
else:
117113
return False
118114

115+
def default_mirror_scheme(self, ssl_key: Optional[str] = None) -> str:
116+
ssl_key = self.ssl_key if ssl_key is None else ssl_key
117+
return "http" if ssl_key is None else "https"
118+
119+
def default_mirror_netloc(
120+
self,
121+
host: Optional[Union[List[str], str]] = None,
122+
port: Optional[int] = None,
123+
) -> str:
124+
host = self.host if host is None else host
125+
port = self.port if port is None else port
126+
default_host = host if self._is_specific_addr(host) else "localhost"
127+
return f"{default_host}:{port}"
128+
119129
def hf_url_base(self) -> str:
120130
return f"{self.hf_scheme}://{self.hf_netloc}"
121131

@@ -139,6 +149,9 @@ def read_toml(self, path: str) -> None:
139149

140150
if "basic" in config:
141151
basic = config["basic"]
152+
previous_default_mirror_scheme = self.default_mirror_scheme()
153+
previous_default_mirror_netloc = self.default_mirror_netloc()
154+
142155
self.host = basic.get("host", self.host)
143156
self.port = basic.get("port", self.port)
144157
self.ssl_key = self.empty_str(basic.get("ssl-key", self.ssl_key))
@@ -151,11 +164,20 @@ def read_toml(self, path: str) -> None:
151164
self.hf_netloc = basic.get("hf-netloc", self.hf_netloc)
152165
self.hf_lfs_netloc = basic.get("hf-lfs-netloc", self.hf_lfs_netloc)
153166

154-
self.mirror_scheme = basic.get("mirror-scheme", self.mirror_scheme)
155-
self.mirror_netloc = basic.get("mirror-netloc", self.mirror_netloc)
156-
self.mirror_lfs_netloc = basic.get(
157-
"mirror-lfs-netloc", self.mirror_lfs_netloc
158-
)
167+
if "mirror-scheme" in basic:
168+
self.mirror_scheme = basic["mirror-scheme"]
169+
elif self.mirror_scheme == previous_default_mirror_scheme:
170+
self.mirror_scheme = self.default_mirror_scheme()
171+
172+
if "mirror-netloc" in basic:
173+
self.mirror_netloc = basic["mirror-netloc"]
174+
elif self.mirror_netloc == previous_default_mirror_netloc:
175+
self.mirror_netloc = self.default_mirror_netloc()
176+
177+
if "mirror-lfs-netloc" in basic:
178+
self.mirror_lfs_netloc = basic["mirror-lfs-netloc"]
179+
elif self.mirror_lfs_netloc == previous_default_mirror_netloc:
180+
self.mirror_lfs_netloc = self.default_mirror_netloc()
159181

160182
self.mirrors_path = basic.get("mirrors-path", self.mirrors_path)
161183

src/olah/proxy/files.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ async def _get_file_range_from_remote(
218218
yield raw_chunk
219219
chunk_bytes += len(raw_chunk)
220220

221-
if is_compressed:
221+
if is_compressed or "content-length" not in response.headers:
222222
response_content_length = chunk_bytes
223223
else:
224224
response_content_length = int(response.headers["content-length"])
@@ -420,7 +420,7 @@ async def _resource_etag(hf_url: str, authorization: Optional[str]=None, offline
420420
ret_etag = response.headers["etag"]
421421
else:
422422
ret_etag = f'"{content_hash[:32]}-10"'
423-
except httpx.TimeoutException:
423+
except httpx.HTTPError:
424424
ret_etag = None
425425
return ret_etag
426426

@@ -438,24 +438,28 @@ async def _remote_file_metadata(
438438
headers = {}
439439
if authorization is not None:
440440
headers["authorization"] = authorization
441-
async with httpx.AsyncClient() as client:
442-
response = await client.request(
443-
method="HEAD",
444-
url=hf_url,
445-
headers=headers,
446-
timeout=WORKER_API_TIMEOUT,
447-
follow_redirects=True,
448-
)
441+
try:
442+
async with httpx.AsyncClient() as client:
443+
response = await client.request(
444+
method="HEAD",
445+
url=hf_url,
446+
headers=headers,
447+
timeout=WORKER_API_TIMEOUT,
448+
follow_redirects=True,
449+
)
450+
except (httpx.HTTPError, ValueError):
451+
return None
449452
if response.status_code >= 400:
450453
return None
451454

452455
content_length = response.headers.get("content-length")
453456
if content_length is None:
454457
return None
455-
return RemoteFileMetadata(
456-
file_size=int(content_length),
457-
etag=response.headers.get("etag"),
458-
)
458+
try:
459+
file_size = int(content_length)
460+
except ValueError:
461+
return None
462+
return RemoteFileMetadata(file_size=file_size, etag=response.headers.get("etag"))
459463

460464
async def _file_realtime_stream(
461465
app,

src/olah/server.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@
6565
logger = None
6666

6767

68+
def normalize_server_host(host: Union[str, Sequence[str]]) -> str:
69+
if isinstance(host, str):
70+
host_candidates = host.split(",")
71+
else:
72+
host_candidates = list(host)
73+
74+
normalized_hosts = [str(item).strip() for item in host_candidates if str(item).strip()]
75+
if len(normalized_hosts) == 0:
76+
raise ValueError("Host cannot be empty.")
77+
if len(normalized_hosts) > 1:
78+
raise ValueError(
79+
"Multiple hosts are not supported by uvicorn.run. Please run one Olah instance per host."
80+
)
81+
return normalized_hosts[0]
82+
83+
6884
async def check_connection(url: str) -> bool:
6985
try:
7086
async with httpx.AsyncClient(follow_redirects=True) as client:
@@ -74,7 +90,7 @@ async def check_connection(url: str) -> bool:
7490
timeout=10,
7591
)
7692
return 200 <= response.status_code < 400
77-
except httpx.TimeoutException:
93+
except httpx.HTTPError:
7894
return False
7995

8096

@@ -193,6 +209,9 @@ def is_default_value(namespace, arg_name):
193209
config = OlahConfig(args.config)
194210
else:
195211
config = OlahConfig()
212+
mirror_scheme_is_default = is_default_value(args, "mirror_scheme")
213+
mirror_netloc_is_default = is_default_value(args, "mirror_netloc")
214+
mirror_lfs_netloc_is_default = is_default_value(args, "mirror_lfs_netloc")
196215

197216
if not is_default_value(args, "host"):
198217
config.host = args.host
@@ -222,8 +241,28 @@ def is_default_value(namespace, arg_name):
222241
config.cache_size_limit = convert_to_bytes(args.cache_size_limit)
223242
if not is_default_value(args, "cache_clean_strategy"):
224243
config.cache_clean_strategy = args.cache_clean_strategy
225-
elif not args.has_lfs_site and not is_default_value(args, "mirror_netloc"):
226-
config.mirror_lfs_netloc = args.mirror_netloc
244+
245+
config.host = normalize_server_host(config.host)
246+
247+
if mirror_scheme_is_default:
248+
config.mirror_scheme = config.default_mirror_scheme()
249+
else:
250+
config.mirror_scheme = args.mirror_scheme
251+
252+
if mirror_netloc_is_default:
253+
config.mirror_netloc = config.default_mirror_netloc()
254+
else:
255+
config.mirror_netloc = args.mirror_netloc
256+
257+
if mirror_lfs_netloc_is_default:
258+
if not args.has_lfs_site and not mirror_netloc_is_default:
259+
config.mirror_lfs_netloc = config.mirror_netloc
260+
else:
261+
config.mirror_lfs_netloc = config.default_mirror_netloc()
262+
else:
263+
config.mirror_lfs_netloc = args.mirror_lfs_netloc
264+
265+
config.host = normalize_server_host(config.host)
227266

228267
if is_default_value(args, "host"):
229268
args.host = config.host
@@ -254,10 +293,8 @@ def is_default_value(namespace, arg_name):
254293
if is_default_value(args, "cache_clean_strategy"):
255294
args.cache_clean_strategy = config.cache_clean_strategy
256295

257-
if "," in args.host:
258-
args.host = args.host.split(",")
259-
260-
args.mirror_scheme = config.mirror_scheme = "http" if args.ssl_key is None else "https"
296+
args.host = normalize_server_host(args.host)
297+
config.host = args.host
261298

262299
print(args)
263300
if config.cache_size_limit is not None:

src/olah/server_api_routes.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,16 @@ async def commits_proxy_common(
271271
async def whoami_v2(request: Request):
272272
new_headers = {k.lower(): v for k, v in request.headers.items()}
273273
new_headers["host"] = request.app.state.app_settings.config.hf_netloc
274-
async with httpx.AsyncClient() as client:
275-
response = await client.request(
276-
method="GET",
277-
url=urljoin(request.app.state.app_settings.config.hf_url_base(), "/api/whoami-v2"),
278-
headers=new_headers,
279-
timeout=10,
280-
)
274+
try:
275+
async with httpx.AsyncClient() as client:
276+
response = await client.request(
277+
method="GET",
278+
url=urljoin(request.app.state.app_settings.config.hf_url_base(), "/api/whoami-v2"),
279+
headers=new_headers,
280+
timeout=10,
281+
)
282+
except httpx.HTTPError:
283+
return Response(status_code=504)
281284
response_headers = {k.lower(): v for k, v in response.headers.items()}
282285
response_headers.pop("content-encoding", None)
283286
response_headers.pop("content-length", None)

src/olah/server_file_routes.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import glob
99
import os
10+
import re
1011
import traceback
1112
from typing import Literal
1213

@@ -36,6 +37,13 @@ def _get_logger(app: FastAPI):
3637
return getattr(app.state, "logger", None) or _NullLogger()
3738

3839

40+
def _repo_name_from_cache_path(repo_path: str) -> str:
41+
parts = [part for part in re.split(r"[\\/]+", repo_path) if part]
42+
if len(parts) < 2:
43+
return repo_path
44+
return get_org_repo(parts[-2], parts[-1])
45+
46+
3947
async def file_head_common(
4048
app: FastAPI,
4149
repo_type: str,
@@ -345,9 +353,9 @@ async def repos(request: Request):
345353
datasets_repos = glob.glob(os.path.join(request.app.state.app_settings.config.repos_path, "api/datasets/*/*"))
346354
models_repos = glob.glob(os.path.join(request.app.state.app_settings.config.repos_path, "api/models/*/*"))
347355
spaces_repos = glob.glob(os.path.join(request.app.state.app_settings.config.repos_path, "api/spaces/*/*"))
348-
datasets_repos = [get_org_repo(*repo.split("/")[-2:]) for repo in datasets_repos]
349-
models_repos = [get_org_repo(*repo.split("/")[-2:]) for repo in models_repos]
350-
spaces_repos = [get_org_repo(*repo.split("/")[-2:]) for repo in spaces_repos]
356+
datasets_repos = [_repo_name_from_cache_path(repo) for repo in datasets_repos]
357+
models_repos = [_repo_name_from_cache_path(repo) for repo in models_repos]
358+
spaces_repos = [_repo_name_from_cache_path(repo) for repo in spaces_repos]
351359

352360
return request.app.state.templates.TemplateResponse(
353361
"repos.html",

src/olah/server_upstream.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ def refresh_cache(self) -> bool:
2020

2121

2222
async def get_latest_commit(app, repo: RepoRef, authorization: Optional[str]) -> Optional[str]:
23-
if app.state.app_settings.config.offline:
24-
return "main"
2523
return await get_newest_commit_hf(
2624
app,
2725
repo.repo_type,

src/olah/utils/repo_utils.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ async def get_newest_commit_hf(
213213
return await get_newest_commit_hf_offline(app, repo_type, org, repo)
214214
obj = json.loads(response.text)
215215
return obj.get("sha", None)
216-
except httpx.TimeoutException as e:
216+
except (httpx.HTTPError, ValueError, OSError):
217217
return await get_newest_commit_hf_offline(app, repo_type, org, repo)
218218

219219

@@ -293,7 +293,7 @@ async def get_commit_hf(
293293
return await get_commit_hf_offline(app, repo_type, org, repo, commit)
294294
obj = json.loads(response.text)
295295
return obj.get("sha", None)
296-
except:
296+
except (httpx.HTTPError, ValueError, OSError):
297297
return await get_commit_hf_offline(app, repo_type, org, repo, commit)
298298

299299

@@ -335,7 +335,15 @@ async def check_commit_hf(
335335
headers = {}
336336
if authorization is not None:
337337
headers["authorization"] = authorization
338-
async with httpx.AsyncClient() as client:
339-
response = await client.request(method="HEAD", url=url, headers=headers, timeout=WORKER_API_TIMEOUT)
340-
status_code = response.status_code
338+
try:
339+
async with httpx.AsyncClient() as client:
340+
response = await client.request(
341+
method="HEAD",
342+
url=url,
343+
headers=headers,
344+
timeout=WORKER_API_TIMEOUT,
345+
)
346+
status_code = response.status_code
347+
except httpx.HTTPError:
348+
return False
341349
return status_code in [200, 307]

src/olah/utils/url_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ def parse_range_params(range_header: str) -> Tuple[str, List[Tuple[Optional[int]
143143
def get_all_ranges(file_size: int, unit: str, ranges: List[Tuple[Optional[int], Optional[int]]], suffix: Optional[int]) -> List[Tuple[int, int]]:
144144
all_ranges: List[Tuple[int, int]] = []
145145
if suffix is not None:
146-
all_ranges.append((file_size - suffix, file_size))
146+
if file_size <= 0 or suffix <= 0:
147+
return all_ranges
148+
all_ranges.append((max(0, file_size - suffix), file_size))
147149
else:
148150
for r in ranges:
149151
r_start = r[0] if r[0] is not None else 0
@@ -259,4 +261,4 @@ def clean_path(path: str) -> str:
259261
path = path.replace("\\", "/")
260262
while "//" in path:
261263
path = path.replace("//", "/")
262-
return path
264+
return path

tests/test_configs.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,43 @@ def test_olah_config_reads_toml_and_normalizes_empty_ssl_fields(tmp_path):
8282
assert config.cache.allow("team/project") is True
8383

8484

85+
def test_olah_config_derives_mirror_netloc_from_updated_host_and_port(tmp_path):
86+
config_path = tmp_path / "config.toml"
87+
config_path.write_text(
88+
textwrap.dedent(
89+
"""
90+
[basic]
91+
host = "0.0.0.0"
92+
port = 9001
93+
"""
94+
),
95+
encoding="utf-8",
96+
)
97+
98+
config = OlahConfig(str(config_path))
99+
100+
assert config.mirror_netloc == "localhost:9001"
101+
assert config.mirror_lfs_netloc == "localhost:9001"
102+
103+
104+
def test_olah_config_derives_https_mirror_scheme_when_ssl_is_configured(tmp_path):
105+
config_path = tmp_path / "config.toml"
106+
config_path.write_text(
107+
textwrap.dedent(
108+
"""
109+
[basic]
110+
ssl-key = "/tmp/key.pem"
111+
ssl-cert = "/tmp/cert.pem"
112+
"""
113+
),
114+
encoding="utf-8",
115+
)
116+
117+
config = OlahConfig(str(config_path))
118+
119+
assert config.mirror_scheme == "https"
120+
121+
85122
def test_is_specific_addr_only_accepts_real_single_host():
86123
config = OlahConfig()
87124

0 commit comments

Comments
 (0)