Skip to content

Commit 8fa5df4

Browse files
authored
Add support for files endpoints (#2)
* Add support for files endpoints * Incorporate feedback from review * Add documentation comments for File APIs * Remove unnecessary @available annotation * Fix API usage in README * Rename downloadFileContents to downloadContentsOfFile
1 parent 6d9444a commit 8fa5df4

File tree

6 files changed

+1333
-5
lines changed

6 files changed

+1333
-5
lines changed

README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,122 @@ _ = try await client.updateDiscussionStatus(
378378
)
379379
```
380380

381+
#### File Operations
382+
383+
```swift
384+
// List files in a repository
385+
let files = try await client.listFiles(
386+
in: "facebook/bart-large",
387+
kind: .model,
388+
revision: "main",
389+
recursive: true
390+
)
391+
392+
for file in files {
393+
if file.type == .file {
394+
print("\(file.path) - \(file.size ?? 0) bytes")
395+
}
396+
}
397+
398+
// Check if a file exists
399+
let exists = await client.fileExists(
400+
at: "README.md",
401+
in: "facebook/bart-large"
402+
)
403+
404+
// Get file information
405+
let file = try await client.getFile(
406+
at: "pytorch_model.bin",
407+
in: "facebook/bart-large"
408+
)
409+
print("File size: \(file.size ?? 0)")
410+
print("Is LFS: \(file.isLFS)")
411+
412+
// Download file data
413+
let data = try await client.downloadContentsOfFile(
414+
at: "config.json",
415+
from: "openai-community/gpt2"
416+
)
417+
let config = try JSONDecoder().decode(ModelConfig.self, from: data)
418+
419+
// Download file to disk
420+
let destination = FileManager.default.temporaryDirectory
421+
.appendingPathComponent("model.safetensors")
422+
423+
let fileURL = try await client.downloadFile(
424+
at: "model.safetensors",
425+
from: "openai-community/gpt2",
426+
to: destination
427+
)
428+
429+
// Download with progress tracking
430+
let progress = Progress(totalUnitCount: 0)
431+
Task {
432+
for await _ in progress.values(forKeyPath: \.fractionCompleted) {
433+
print("Download progress: \(progress.fractionCompleted * 100)%")
434+
}
435+
}
436+
437+
let fileURL = try await client.downloadFile(
438+
at: "pytorch_model.bin",
439+
from: "facebook/bart-large",
440+
to: destination,
441+
progress: progress
442+
)
443+
444+
// Resume a download
445+
let resumeData: Data = // ... from previous download
446+
let fileURL = try await client.resumeDownloadFile(
447+
resumeData: resumeData,
448+
to: destination,
449+
progress: progress
450+
)
451+
452+
// Upload a file
453+
let result = try await client.uploadFile(
454+
URL(fileURLWithPath: "/path/to/local/file.csv"),
455+
to: "data/new_dataset.csv",
456+
in: "username/my-dataset",
457+
kind: .dataset,
458+
branch: "main",
459+
message: "Add new dataset"
460+
)
461+
print("Uploaded to: \(result.path)")
462+
463+
// Upload multiple files in a batch
464+
let results = try await client.uploadFiles(
465+
[
466+
"README.md": .path("/path/to/readme.md"),
467+
"data.json": .path("/path/to/data.json"),
468+
],
469+
to: "username/my-repo",
470+
message: "Initial commit",
471+
maxConcurrent: 3
472+
)
473+
474+
// Or build a batch programmatically
475+
var batch = FileBatch()
476+
batch["config.json"] = .path("/path/to/config.json")
477+
batch["model.safetensors"] = .url(
478+
URL(fileURLWithPath: "/path/to/model.safetensors"),
479+
mimeType: "application/octet-stream"
480+
)
481+
482+
// Delete a file
483+
try await client.deleteFile(
484+
at: "old_file.txt",
485+
from: "username/my-repo",
486+
message: "Remove old file"
487+
)
488+
489+
// Delete multiple files
490+
try await client.deleteFiles(
491+
at: ["file1.txt", "file2.txt", "old_dir/file3.txt"],
492+
from: "username/my-repo",
493+
message: "Cleanup old files"
494+
)
495+
```
496+
381497
#### User Access Management
382498

383499
```swift

Sources/HuggingFace/Hub/File.swift

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import Foundation
2+
3+
/// Information about a file in a repository.
4+
public struct File: Hashable, Codable, Sendable {
5+
/// A Boolean value indicating whether the file exists in the repository.
6+
public let exists: Bool
7+
8+
/// The size of the file in bytes.
9+
public let size: Int64?
10+
11+
/// The entity tag (ETag) for the file, used for caching and change detection.
12+
public let etag: String?
13+
14+
/// The Git revision (commit SHA) at which this file information was retrieved.
15+
public let revision: String?
16+
17+
/// A Boolean value indicating whether the file is stored using Git Large File Storage (LFS).
18+
public let isLFS: Bool
19+
20+
init(
21+
exists: Bool,
22+
size: Int64? = nil,
23+
etag: String? = nil,
24+
revision: String? = nil,
25+
isLFS: Bool = false
26+
) {
27+
self.exists = exists
28+
self.size = size
29+
self.etag = etag
30+
self.revision = revision
31+
self.isLFS = isLFS
32+
}
33+
}
34+
35+
// MARK: -
36+
37+
/// A collection of files to upload in a batch operation.
38+
///
39+
/// Use `FileBatch` to prepare multiple files for uploading to a repository in a single operation.
40+
/// You can add files using subscript notation or dictionary literal syntax.
41+
///
42+
/// ```swift
43+
/// var batch = FileBatch()
44+
/// batch["config.json"] = .path("/path/to/config.json")
45+
/// batch["model.safetensors"] = .url(
46+
/// URL(fileURLWithPath: "/path/to/model.safetensors"),
47+
/// mimeType: "application/octet-stream"
48+
/// )
49+
/// let _ = try await client.uploadFiles(batch, to: "username/my-repo", message: "Initial commit")
50+
/// ```
51+
/// - SeeAlso: `HubClient.uploadFiles(_:to:kind:branch:message:maxConcurrent:)`
52+
public struct FileBatch: Hashable, Codable, Sendable {
53+
/// An entry representing a file to upload.
54+
public struct Entry: Hashable, Codable, Sendable {
55+
/// The file URL pointing to the local file to upload.
56+
public var url: URL
57+
58+
/// The MIME type of the file.
59+
public var mimeType: String?
60+
61+
private init(url: URL, mimeType: String? = nil) {
62+
self.url = url
63+
self.mimeType = mimeType
64+
}
65+
66+
/// Creates a file entry from a file system path.
67+
/// - Parameters:
68+
/// - path: The file system path to the local file.
69+
/// - mimeType: The MIME type of the file. If not provided, the MIME type is inferred from the file extension.
70+
/// - Returns: A file entry for the specified path.
71+
public static func path(_ path: String, mimeType: String? = nil) -> Self {
72+
return Self(url: URL(fileURLWithPath: path), mimeType: mimeType)
73+
}
74+
75+
/// Creates a file entry from a URL.
76+
/// - Parameters:
77+
/// - url: The file URL. Must be a file URL (e.g., `file:///path/to/file`), not a remote URL.
78+
/// - mimeType: Optional MIME type for the file.
79+
/// - Returns: A file entry, or `nil` if the URL is not a file URL.
80+
/// - Note: Only file URLs are accepted because this API requires local file access for upload.
81+
/// Remote URLs (http, https, etc.) are not supported and will return `nil`.
82+
public static func url(_ url: URL, mimeType: String? = nil) -> Self? {
83+
guard url.isFileURL else {
84+
return nil
85+
}
86+
return Self(url: url, mimeType: mimeType)
87+
}
88+
}
89+
90+
private var entries: [String: Entry]
91+
92+
/// Creates an empty file batch.
93+
public init() {
94+
self.entries = [:]
95+
}
96+
97+
/// Creates a file batch with the specified entries.
98+
/// - Parameter entries: A dictionary mapping repository paths to file entries.
99+
public init(_ entries: [String: Entry]) {
100+
self.entries = entries
101+
}
102+
103+
/// Accesses the file entry for the specified repository path.
104+
/// - Parameter path: The path in the repository where the file will be uploaded.
105+
/// - Returns: The file entry for the specified path, or `nil` if no entry exists.
106+
public subscript(path: String) -> Entry? {
107+
get {
108+
return entries[path]
109+
}
110+
set {
111+
entries[path] = newValue
112+
}
113+
}
114+
}
115+
116+
// MARK: - Collection
117+
118+
extension FileBatch: Swift.Collection {
119+
public typealias Index = Dictionary<String, Entry>.Index
120+
121+
public var startIndex: Index { entries.startIndex }
122+
public var endIndex: Index { entries.endIndex }
123+
public func index(after i: Index) -> Index { entries.index(after: i) }
124+
public subscript(position: Index) -> (key: String, value: Entry) { entries[position] }
125+
126+
public func makeIterator() -> Dictionary<String, Entry>.Iterator {
127+
return entries.makeIterator()
128+
}
129+
}
130+
131+
// MARK: - ExpressibleByDictionaryLiteral
132+
133+
extension FileBatch: ExpressibleByDictionaryLiteral {
134+
public init(dictionaryLiteral elements: (String, Entry)...) {
135+
self.init(Dictionary(uniqueKeysWithValues: elements))
136+
}
137+
}

0 commit comments

Comments
 (0)