Skip to content

Commit fae7768

Browse files
Fix Hub API URL construction (#295)
1 parent 18ccbd2 commit fae7768

File tree

2 files changed

+52
-10
lines changed

2 files changed

+52
-10
lines changed

Sources/Hub/HubApi.swift

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ private extension HubApi {
152152
)
153153
}
154154
},
155-
{ try? String(contentsOf: .homeDirectory.appendingPathComponent(".cache/huggingface/token"), encoding: .utf8) },
156-
{ try? String(contentsOf: .homeDirectory.appendingPathComponent(".huggingface/token"), encoding: .utf8) },
155+
{ try? String(contentsOf: .homeDirectory.appending(path: ".cache/huggingface/token"), encoding: .utf8) },
156+
{ try? String(contentsOf: .homeDirectory.appending(path: ".huggingface/token"), encoding: .utf8) },
157157
]
158158
return possibleTokens
159159
.lazy
@@ -258,7 +258,12 @@ public extension HubApi {
258258
/// - Throws: HubClientError if the repository cannot be accessed or parsed
259259
func getFilenames(from repo: Repo, revision: String = "main", matching globs: [String] = []) async throws -> [String] {
260260
// Read repo info and only parse "siblings"
261-
let url = URL(string: "\(endpoint)/api/\(repo.type)/\(repo.id)/revision/\(revision)")!
261+
let url = URL(string: endpoint)!
262+
.appending(path: "api")
263+
.appending(path: repo.type.rawValue)
264+
.appending(path: repo.id)
265+
.appending(path: "revision")
266+
.appending(component: revision) // Encode slashes (e.g., "pr/1" -> "pr%2F1")
262267
let (data, _) = try await httpGet(for: url)
263268
let response = try JSONDecoder().decode(SiblingsResponse.self, from: data)
264269
let filenames = response.siblings.map { $0.rfilename }
@@ -333,7 +338,9 @@ public extension HubApi {
333338
func whoami() async throws -> Config {
334339
guard hfToken != nil else { throw Hub.HubClientError.authorizationRequired }
335340

336-
let url = URL(string: "\(endpoint)/api/whoami-v2")!
341+
let url = URL(string: endpoint)!
342+
.appending(path: "api")
343+
.appending(path: "whoami-v2")
337344
let (data, _) = try await httpGet(for: url)
338345

339346
let parsed = try JSONSerialization.jsonObject(with: data, options: [])
@@ -462,10 +469,11 @@ public extension HubApi {
462469
// https://huggingface.co/coreml-projects/Llama-2-7b-chat-coreml/resolve/main/tokenizer.json?download=true
463470
var url = URL(string: endpoint ?? "https://huggingface.co")!
464471
if repo.type != .models {
465-
url = url.appending(component: repo.type.rawValue)
472+
url = url.appending(path: repo.type.rawValue)
466473
}
467474
url = url.appending(path: repo.id)
468-
url = url.appending(path: "resolve/\(revision)")
475+
url = url.appending(path: "resolve")
476+
url = url.appending(component: revision) // Encode slashes (e.g., "pr/1" -> "pr%2F1")
469477
url = url.appending(path: relativeFilename)
470478
return url
471479
}
@@ -579,9 +587,9 @@ public extension HubApi {
579587
let repoDestination = localRepoLocation(repo)
580588
let repoMetadataDestination =
581589
repoDestination
582-
.appendingPathComponent(".cache")
583-
.appendingPathComponent("huggingface")
584-
.appendingPathComponent("download")
590+
.appending(path: ".cache")
591+
.appending(path: "huggingface")
592+
.appending(path: "download")
585593

586594
let shouldUseOfflineMode = await NetworkMonitor.shared.state.shouldUseOfflineMode()
587595

@@ -792,7 +800,10 @@ public extension HubApi {
792800

793801
func getFileMetadata(from repo: Repo, revision: String = "main", matching globs: [String] = []) async throws -> [FileMetadata] {
794802
let files = try await getFilenames(from: repo, matching: globs)
795-
let url = URL(string: "\(endpoint)/\(repo.id)/resolve/\(revision)")!
803+
let url = URL(string: endpoint)!
804+
.appending(path: repo.id)
805+
.appending(path: "resolve")
806+
.appending(component: revision) // Encode slashes (e.g., "pr/1" -> "pr%2F1")
796807
var selectedMetadata: [FileMetadata] = []
797808
for file in files {
798809
let fileURL = url.appending(path: file)

Tests/HubTests/HubApiTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,29 @@ import XCTest
1111
class HubApiTests: XCTestCase {
1212
// TODO: use a specific revision for these tests
1313

14+
/// Test that revision values containing slashes (like "pr/1") are properly URL encoded.
15+
/// The Hub API requires "pr/1" to be encoded as "pr%2F1" - otherwise it returns 404.
16+
func testGetFilenamesWithPRRevision() async throws {
17+
let hubApi = HubApi()
18+
let filenames = try await hubApi.getFilenames(
19+
from: Hub.Repo(id: "coreml-projects/sam-2-studio"),
20+
revision: "pr/1",
21+
matching: ["*.md"]
22+
)
23+
XCTAssertFalse(filenames.isEmpty, "Should retrieve filenames from PR revision")
24+
}
25+
26+
/// Test that getFileMetadata works with PR revision format.
27+
func testGetFileMetadataWithPRRevision() async throws {
28+
let hubApi = HubApi()
29+
let metadata = try await hubApi.getFileMetadata(
30+
from: Hub.Repo(id: "coreml-projects/sam-2-studio"),
31+
revision: "pr/1",
32+
matching: ["*.md"]
33+
)
34+
XCTAssertFalse(metadata.isEmpty, "Should retrieve file metadata from PR revision")
35+
}
36+
1437
func testFilenameRetrieval() async {
1538
do {
1639
let filenames = try await Hub.getFilenames(from: "coreml-projects/Llama-2-7b-chat-coreml")
@@ -1181,4 +1204,12 @@ class SnapshotDownloadTests: XCTestCase {
11811204
XCTFail("Unexpected error: \(error)")
11821205
}
11831206
}
1207+
1208+
/// Test that snapshot download works with PR revision format.
1209+
func testDownloadWithPRRevision() async throws {
1210+
let hubApi = HubApi(downloadBase: downloadDestination)
1211+
let prRepo = "coreml-projects/sam-2-studio"
1212+
let downloadedTo = try await hubApi.snapshot(from: prRepo, revision: "pr/1", matching: "*.md")
1213+
XCTAssertTrue(FileManager.default.fileExists(atPath: downloadedTo.path))
1214+
}
11841215
}

0 commit comments

Comments
 (0)