Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .swift-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6.1.3
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"python-envs.defaultEnvManager": "ms-python.python:pyenv",
"python-envs.pythonProjects": []
}
33 changes: 33 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/mattt/EventSource.git", from: "1.0.0")
.package(url: "https://github.com/mattt/EventSource.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "5.0.0"),
],
targets: [
.target(
name: "HuggingFace",
dependencies: [
.product(name: "EventSource", package: "EventSource")
.product(name: "EventSource", package: "EventSource"),
.product(name: "Crypto", package: "swift-crypto"),
],
path: "Sources/HuggingFace"
),
Expand Down
1 change: 0 additions & 1 deletion Sources/HuggingFace/Hub/File.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import CryptoKit
import Foundation

/// Information about a file in a repository.
Expand Down
178 changes: 122 additions & 56 deletions Sources/HuggingFace/Hub/HubClient+Files.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Foundation
import UniformTypeIdentifiers

#if canImport(UniformTypeIdentifiers)
import UniformTypeIdentifiers
#endif

#if canImport(FoundationNetworking)
import FoundationNetworking
Expand Down Expand Up @@ -78,7 +81,11 @@ public extension HubClient {
.buildToTempFile()
defer { try? FileManager.default.removeItem(at: tempFile) }

let (data, response) = try await session.upload(for: request, fromFile: tempFile)
#if canImport(FoundationNetworking)
let (data, response) = try await session.asyncUpload(for: request, fromFile: tempFile)
#else
let (data, response) = try await session.upload(for: request, fromFile: tempFile)
#endif
_ = try httpClient.validateResponse(response, data: data)

if data.isEmpty {
Expand All @@ -95,7 +102,11 @@ public extension HubClient {
.addFile(name: "file", fileURL: fileURL, mimeType: mimeType)
.buildInMemory()

let (data, response) = try await session.upload(for: request, from: body)
#if canImport(FoundationNetworking)
let (data, response) = try await session.asyncUpload(for: request, from: body)
#else
let (data, response) = try await session.upload(for: request, from: body)
#endif
_ = try httpClient.validateResponse(response, data: data)

if data.isEmpty {
Expand Down Expand Up @@ -210,7 +221,11 @@ public extension HubClient {
var request = try await httpClient.createRequest(.get, url: url)
request.cachePolicy = cachePolicy

let (data, response) = try await session.data(for: request)
#if canImport(FoundationNetworking)
let (data, response) = try await session.asyncData(for: request)
#else
let (data, response) = try await session.data(for: request)
#endif
_ = try httpClient.validateResponse(response, data: data)

// Store in cache if we have etag and commit info
Expand Down Expand Up @@ -285,10 +300,14 @@ public extension HubClient {
var request = try await httpClient.createRequest(.get, url: url)
request.cachePolicy = cachePolicy

let (tempURL, response) = try await session.download(
for: request,
delegate: progress.map { DownloadProgressDelegate(progress: $0) }
)
#if canImport(FoundationNetworking)
let (tempURL, response) = try await session.asyncDownload(for: request, progress: progress)
#else
let (tempURL, response) = try await session.download(
for: request,
delegate: progress.map { DownloadProgressDelegate(progress: $0) }
)
#endif
_ = try httpClient.validateResponse(response, data: nil)

// Store in cache before moving to destination
Expand Down Expand Up @@ -321,29 +340,35 @@ public extension HubClient {
return destination
}

/// Download file with resume capability
/// - Parameters:
/// - resumeData: Resume data from a previous download attempt
/// - destination: Destination URL for downloaded file
/// - progress: Optional Progress object to track download progress
/// - Returns: Final destination URL
func resumeDownloadFile(
resumeData: Data,
to destination: URL,
progress: Progress? = nil
) async throws -> URL {
let (tempURL, response) = try await session.download(
resumeFrom: resumeData,
delegate: progress.map { DownloadProgressDelegate(progress: $0) }
)
_ = try httpClient.validateResponse(response, data: nil)
#if !canImport(FoundationNetworking)
/// Download file with resume capability
///
/// - Note: This method is only available on Apple platforms.
/// On Linux, resume functionality is not supported.
///
/// - Parameters:
/// - resumeData: Resume data from a previous download attempt
/// - destination: Destination URL for downloaded file
/// - progress: Optional Progress object to track download progress
/// - Returns: Final destination URL
func resumeDownloadFile(
resumeData: Data,
to destination: URL,
progress: Progress? = nil
) async throws -> URL {
let (tempURL, response) = try await session.download(
resumeFrom: resumeData,
delegate: progress.map { DownloadProgressDelegate(progress: $0) }
)
_ = try httpClient.validateResponse(response, data: nil)

// Move from temporary location to final destination
try? FileManager.default.removeItem(at: destination)
try FileManager.default.moveItem(at: tempURL, to: destination)
// Move from temporary location to final destination
try? FileManager.default.removeItem(at: destination)
try FileManager.default.moveItem(at: tempURL, to: destination)

return destination
}
return destination
}
#endif

/// Download file to a destination URL (convenience method without progress tracking)
/// - Parameters:
Expand Down Expand Up @@ -379,32 +404,34 @@ public extension HubClient {

// MARK: - Progress Delegate

private final class DownloadProgressDelegate: NSObject, URLSessionDownloadDelegate, @unchecked Sendable {
private let progress: Progress
#if !canImport(FoundationNetworking)
private final class DownloadProgressDelegate: NSObject, URLSessionDownloadDelegate, @unchecked Sendable {
private let progress: Progress

init(progress: Progress) {
self.progress = progress
}
init(progress: Progress) {
self.progress = progress
}

func urlSession(
_: URLSession,
downloadTask _: URLSessionDownloadTask,
didWriteData _: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64
) {
progress.totalUnitCount = totalBytesExpectedToWrite
progress.completedUnitCount = totalBytesWritten
}
func urlSession(
_: URLSession,
downloadTask _: URLSessionDownloadTask,
didWriteData _: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64
) {
progress.totalUnitCount = totalBytesExpectedToWrite
progress.completedUnitCount = totalBytesWritten
}

func urlSession(
_: URLSession,
downloadTask _: URLSessionDownloadTask,
didFinishDownloadingTo _: URL
) {
// The actual file handling is done in the async/await layer
func urlSession(
_: URLSession,
downloadTask _: URLSessionDownloadTask,
didFinishDownloadingTo _: URL
) {
// The actual file handling is done in the async/await layer
}
}
}
#endif

// MARK: - Delete Operations

Expand Down Expand Up @@ -531,7 +558,11 @@ public extension HubClient {
request.setValue("bytes=0-0", forHTTPHeaderField: "Range")

do {
let (_, response) = try await session.data(for: request)
#if canImport(FoundationNetworking)
let (_, response) = try await session.asyncData(for: request)
#else
let (_, response) = try await session.data(for: request)
#endif
guard let httpResponse = response as? HTTPURLResponse else {
return File(exists: false)
}
Expand Down Expand Up @@ -632,9 +663,44 @@ private struct UploadResponse: Codable {

private extension URL {
var mimeType: String? {
guard let uti = UTType(filenameExtension: pathExtension) else {
return nil
}
return uti.preferredMIMEType
#if canImport(UniformTypeIdentifiers)
guard let uti = UTType(filenameExtension: pathExtension) else {
return nil
}
return uti.preferredMIMEType
#else
// Fallback MIME type lookup for Linux
let ext = pathExtension.lowercased()
switch ext {
case "json": return "application/json"
case "txt": return "text/plain"
case "html", "htm": return "text/html"
case "css": return "text/css"
case "js": return "application/javascript"
case "xml": return "application/xml"
case "pdf": return "application/pdf"
case "zip": return "application/zip"
case "gz", "gzip": return "application/gzip"
case "tar": return "application/x-tar"
case "png": return "image/png"
case "jpg", "jpeg": return "image/jpeg"
case "gif": return "image/gif"
case "svg": return "image/svg+xml"
case "webp": return "image/webp"
case "mp3": return "audio/mpeg"
case "wav": return "audio/wav"
case "mp4": return "video/mp4"
case "webm": return "video/webm"
case "bin", "safetensors", "gguf", "ggml": return "application/octet-stream"
case "pt", "pth": return "application/octet-stream"
case "onnx": return "application/octet-stream"
case "md": return "text/markdown"
case "yaml", "yml": return "application/x-yaml"
case "toml": return "application/toml"
case "py": return "text/x-python"
case "swift": return "text/x-swift"
default: return "application/octet-stream"
}
#endif
}
}
67 changes: 40 additions & 27 deletions Sources/HuggingFace/Hub/Pagination.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

/// Sort direction for list queries.
public enum SortDirection: Int, Hashable, Sendable {
Expand Down Expand Up @@ -28,38 +35,44 @@ public struct PaginatedResponse<T: Decodable & Sendable>: Sendable {
}
}

// MARK: -
// MARK: - Link Header Parsing

extension HTTPURLResponse {
/// Parses the Link header to extract the next page URL.
///
/// The Link header format follows RFC 8288: `<url>; rel="next"`
///
/// - Returns: The URL for the next page, or `nil` if not found.
func nextPageURL() -> URL? {
guard let linkHeader = value(forHTTPHeaderField: "Link") else {
return nil
}
/// Parses the Link header from an HTTP response to extract the next page URL.
///
/// The Link header format follows RFC 8288: `<url>; rel="next"`
///
/// - Parameter response: The HTTP response to parse.
/// - Returns: The URL for the next page, or `nil` if not found.
func parseNextPageURL(from response: HTTPURLResponse) -> URL? {
guard let linkHeader = response.value(forHTTPHeaderField: "Link") else {
return nil
}
return parseNextPageURL(fromLinkHeader: linkHeader)
}

// Parse Link header format: <https://example.com/page2>; rel="next"
let links = linkHeader.components(separatedBy: ",")
for link in links {
let components = link.components(separatedBy: ";")
guard components.count >= 2 else { continue }
/// Parses a Link header string to extract the next page URL.
///
/// - Parameter linkHeader: The Link header value.
/// - Returns: The URL for the next page, or `nil` if not found.
func parseNextPageURL(fromLinkHeader linkHeader: String) -> URL? {
// Parse Link header format: <https://example.com/page2>; rel="next"
let links = linkHeader.components(separatedBy: ",")
for link in links {
let components = link.components(separatedBy: ";")
guard components.count >= 2 else { continue }

let urlPart = components[0].trimmingCharacters(in: .whitespaces)
let relPart = components[1].trimmingCharacters(in: .whitespaces)
let urlPart = components[0].trimmingCharacters(in: .whitespaces)
let relPart = components[1].trimmingCharacters(in: .whitespaces)

// Check if this is the "next" link
if relPart.contains("rel=\"next\"") || relPart.contains("rel='next'") {
// Extract URL from angle brackets
let urlString = urlPart.trimmingCharacters(in: CharacterSet(charactersIn: "<>"))
if let url = URL(string: urlString) {
return url
}
// Check if this is the "next" link
if relPart.contains("rel=\"next\"") || relPart.contains("rel='next'") {
// Extract URL from angle brackets
let urlString = urlPart.trimmingCharacters(in: CharacterSet(charactersIn: "<>"))
if let url = URL(string: urlString) {
return url
}
}

return nil
}

return nil
}
Loading
Loading