Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
eb882bd
Added swift-crypto to replace CryptoKit on Linux. Renamed imports to …
Dec 8, 2025
f7f50f9
HTTPURLResponse on Linux is made available via FoundationNetworking. …
Dec 8, 2025
09a8b30
Removed precompile #if because we are including the Crypto library no…
Dec 8, 2025
8f4d3d4
Replaced use of SecRandomCopyBytes by SystemRandomNumberGenerator on …
Dec 8, 2025
027e945
The download overload to resume a download from a Data object is not …
Dec 8, 2025
ad0756a
Added other Apple platforms to the precompiler directive
Dec 8, 2025
a46b33a
Added an alternative for the streaming URLRequest.bytes(for:) function.
Dec 8, 2025
4dcf846
Added conditional import of FoundationNetworking for all tests that n…
Dec 8, 2025
a723952
builds and tests pass on Linux
pedronahum Dec 8, 2025
5bd7223
Merge branch 'pr-1' into linux_fixes
Dec 9, 2025
05cbc02
Set swift version to 6.1 now to make everything compile.
Dec 9, 2025
7b352b9
Merge branch 'main' into linux_fixes
Dec 9, 2025
d8b4b27
Removed Streaming.swift as it wasn't used any more.
Dec 9, 2025
d9258fb
Fixed tests running slowly. Forgot to uncomment some code
Dec 9, 2025
6b1bcdc
We need asyncData only when streaming
Dec 11, 2025
2478546
Added a bit of debugging to HTTPCLient.
Dec 11, 2025
8e9b4ed
Ignore gated for now
Dec 11, 2025
9e4033b
Dataset parsing should be ok
Dec 11, 2025
5a3c0e3
Delete .vscode
mattt Dec 16, 2025
61ddcfa
Delete .swift-version
mattt Dec 16, 2025
ea6f75b
Rename asyncUpload to upload
mattt Dec 16, 2025
83368d5
Remove duplicate import statement
mattt Dec 16, 2025
d0d888b
Rename parseNextPageURL to consistently use from: label with type ove…
mattt Dec 16, 2025
01786df
Remove debug print statement
mattt Dec 16, 2025
de7bf73
Fix sendability issues
mattt Dec 16, 2025
1cbad37
Update CI workflow to test on Linux
mattt Dec 16, 2025
5dcb623
swift format . -i -r
mattt Dec 16, 2025
6487650
Update test RequestHandlerStorage to use dictionary of checked contin…
mattt Dec 16, 2025
b5c4574
Ensure consistent URL creation for empty strings across platforms
mattt Dec 16, 2025
c59ba46
Rename asyncData to data
mattt Dec 16, 2025
803764a
Update test expectations
mattt Dec 16, 2025
01b79c9
Switch MockURLProtocol from actor to class with NSLock
mattt Dec 16, 2025
d0106ca
Organize fallback MIME type mapping
mattt Dec 16, 2025
9279a9a
Add more MIME fallbacks
mattt Dec 16, 2025
e6e0943
Fix PKCE random number generation
mattt Dec 16, 2025
3a80328
Use !os(Windows) for conditional compilation
mattt Dec 16, 2025
1a3e326
Remove duplicate import statements
mattt Dec 16, 2025
e207f9d
Fix CI workflow
mattt Dec 16, 2025
070e209
Give caveats more prominence in asyncBytes documentation
mattt Dec 16, 2025
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
51 changes: 44 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ on:
branches: ["main"]

jobs:
test:
name: Swift ${{ matrix.swift }} on Xcode ${{ matrix.xcode }}
runs-on: ${{ matrix.runs-on }}
test-macos:
name: Swift ${{ matrix.swift }} on macOS ${{ matrix.macos }} with Xcode ${{ matrix.xcode }}
runs-on: macos-${{ matrix.macos }}
env:
DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer"
strategy:
Expand All @@ -18,16 +18,53 @@ jobs:
include:
- swift: "6.0"
xcode: "16.0"
runs-on: macos-15
macos: "15"
- swift: "6.1"
xcode: "16.3"
runs-on: macos-15
macos: "15"
- swift: "6.2"
xcode: "26.0"
runs-on: macos-26
macos: "26"
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Cache Swift Package Manager dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/org.swift.swiftpm
.build
key: ${{ runner.os }}-swift-${{ matrix.swift }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-swift-${{ matrix.swift }}-spm-

- name: Build
run: swift build -v

- name: Test
run: swift test -v

test-linux:
name: Swift ${{ matrix.swift-version }} on Linux
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
swift-version:
- "6.0.3"
- "6.1.3"
- "6.2.3"
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Swift
uses: vapor/swiftly-action@v0.2
with:
toolchain: ${{ matrix.swift-version }}

- name: Build
run: swift build -v
Expand Down
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
217 changes: 165 additions & 52 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 @@ -285,10 +288,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 +328,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 +392,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 @@ -632,9 +647,107 @@ 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 {
// MARK: - JSON
case "json":
return "application/json"
// MARK: - Text
case "txt":
return "text/plain"
case "md":
return "text/markdown"
case "csv":
return "text/csv"
case "tsv":
return "text/tab-separated-values"
// MARK: - HTML and Markup
case "html", "htm":
return "text/html"
case "xml":
return "application/xml"
case "svg":
return "image/svg+xml"
case "yaml", "yml":
return "application/x-yaml"
case "toml":
return "application/toml"
// MARK: - Code
case "js":
return "application/javascript"
case "py":
return "text/x-python"
case "swift":
return "text/x-swift"
case "css":
return "text/css"
case "ipynb":
return "application/x-ipynb+json"
// MARK: - Archives and Compressed
case "zip":
return "application/zip"
case "gz", "gzip":
return "application/gzip"
case "tar":
return "application/x-tar"
case "bz2":
return "application/x-bzip2"
case "7z":
return "application/x-7z-compressed"
// MARK: - PDF and Documents
case "pdf":
return "application/pdf"
// MARK: - Images
case "png":
return "image/png"
case "jpg", "jpeg":
return "image/jpeg"
case "gif":
return "image/gif"
case "webp":
return "image/webp"
case "bmp":
return "image/bmp"
case "tiff", "tif":
return "image/tiff"
// MARK: - Audio
case "m4a":
return "audio/mp4"
case "mp3":
return "audio/mpeg"
case "wav":
return "audio/wav"
case "flac":
return "audio/flac"
case "ogg":
return "audio/ogg"
// MARK: - Video
case "mp4":
return "video/mp4"
case "webm":
return "video/webm"
// MARK: - ML/Model/Raw Data
case "bin", "safetensors", "gguf", "ggml":
return "application/octet-stream"
case "pt", "pth":
return "application/octet-stream"
case "onnx":
return "application/octet-stream"
case "ckpt":
return "application/octet-stream"
case "npz":
return "application/octet-stream"
// MARK: - Default
default:
return "application/octet-stream"
}
#endif
}
}
Loading