@@ -60,6 +60,25 @@ public extension HubApi {
60
60
return ( data, response)
61
61
}
62
62
63
+ func httpHead( for url: URL ) async throws -> ( Data , HTTPURLResponse ) {
64
+ var request = URLRequest ( url: url)
65
+ request. httpMethod = " HEAD "
66
+ if let hfToken = hfToken {
67
+ request. setValue ( " Bearer \( hfToken) " , forHTTPHeaderField: " Authorization " )
68
+ }
69
+ request. setValue ( " identity " , forHTTPHeaderField: " Accept-Encoding " )
70
+ let ( data, response) = try await URLSession . shared. data ( for: request)
71
+ guard let response = response as? HTTPURLResponse else { throw Hub . HubClientError. unexpectedError }
72
+
73
+ switch response. statusCode {
74
+ case 200 ..< 300 : break
75
+ case 400 ..< 500 : throw Hub . HubClientError. authorizationRequired
76
+ default : throw Hub . HubClientError. httpStatusCode ( response. statusCode)
77
+ }
78
+
79
+ return ( data, response)
80
+ }
81
+
63
82
func getFilenames( from repo: Repo , matching globs: [ String ] = [ ] ) async throws -> [ String ] {
64
83
// Read repo info and only parse "siblings"
65
84
let url = URL ( string: " \( endpoint) /api/ \( repo. type) / \( repo. id) " ) !
@@ -222,6 +241,65 @@ public extension HubApi {
222
241
}
223
242
}
224
243
244
+ /// Metadata
245
+ public extension HubApi {
246
+ /// A structure representing metadata for a remote file
247
+ struct FileMetadata {
248
+ /// The file's Git commit hash
249
+ public let commitHash : String ?
250
+
251
+ /// Server-provided ETag for caching
252
+ public let etag : String ?
253
+
254
+ /// Stringified URL location of the file
255
+ public let location : String
256
+
257
+ /// The file's size in bytes
258
+ public let size : Int ?
259
+ }
260
+
261
+ private func normalizeEtag( _ etag: String ? ) -> String ? {
262
+ guard let etag = etag else { return nil }
263
+ return etag. trimmingPrefix ( " W/ " ) . trimmingCharacters ( in: CharacterSet ( charactersIn: " \" " ) )
264
+ }
265
+
266
+ func getFileMetadata( url: URL ) async throws -> FileMetadata {
267
+ let ( _, response) = try await httpHead ( for: url)
268
+
269
+ return FileMetadata (
270
+ commitHash: response. value ( forHTTPHeaderField: " X-Repo-Commit " ) ,
271
+ etag: normalizeEtag (
272
+ ( response. value ( forHTTPHeaderField: " X-Linked-Etag " ) ) ?? ( response. value ( forHTTPHeaderField: " Etag " ) )
273
+ ) ,
274
+ location: ( response. value ( forHTTPHeaderField: " Location " ) ) ?? url. absoluteString,
275
+ size: Int ( response. value ( forHTTPHeaderField: " X-Linked-Size " ) ?? response. value ( forHTTPHeaderField: " Content-Length " ) ?? " " )
276
+ )
277
+ }
278
+
279
+ func getFileMetadata( from repo: Repo , matching globs: [ String ] = [ ] ) async throws -> [ FileMetadata ] {
280
+ let files = try await getFilenames ( from: repo, matching: globs)
281
+ let url = URL ( string: " \( endpoint) / \( repo. id) /resolve/main " ) ! // TODO: revisions
282
+ var selectedMetadata : Array < FileMetadata > = [ ]
283
+ for file in files {
284
+ let fileURL = url. appending ( path: file)
285
+ selectedMetadata. append ( try await getFileMetadata ( url: fileURL) )
286
+ }
287
+ return selectedMetadata
288
+ }
289
+
290
+ func getFileMetadata( from repoId: String , matching globs: [ String ] = [ ] ) async throws -> [ FileMetadata ] {
291
+ return try await getFileMetadata ( from: Repo ( id: repoId) , matching: globs)
292
+ }
293
+
294
+ func getFileMetadata( from repo: Repo , matching glob: String ) async throws -> [ FileMetadata ] {
295
+ return try await getFileMetadata ( from: repo, matching: [ glob] )
296
+ }
297
+
298
+ func getFileMetadata( from repoId: String , matching glob: String ) async throws -> [ FileMetadata ] {
299
+ return try await getFileMetadata ( from: Repo ( id: repoId) , matching: [ glob] )
300
+ }
301
+ }
302
+
225
303
/// Stateless wrappers that use `HubApi` instances
226
304
public extension Hub {
227
305
static func getFilenames( from repo: Hub . Repo , matching globs: [ String ] = [ ] ) async throws -> [ String ] {
@@ -259,6 +337,26 @@ public extension Hub {
259
337
static func whoami( token: String ) async throws -> Config {
260
338
return try await HubApi ( hfToken: token) . whoami ( )
261
339
}
340
+
341
+ static func getFileMetadata( fileURL: URL ) async throws -> HubApi . FileMetadata {
342
+ return try await HubApi . shared. getFileMetadata ( url: fileURL)
343
+ }
344
+
345
+ static func getFileMetadata( from repo: Repo , matching globs: [ String ] = [ ] ) async throws -> [ HubApi . FileMetadata ] {
346
+ return try await HubApi . shared. getFileMetadata ( from: repo, matching: globs)
347
+ }
348
+
349
+ static func getFileMetadata( from repoId: String , matching globs: [ String ] = [ ] ) async throws -> [ HubApi . FileMetadata ] {
350
+ return try await HubApi . shared. getFileMetadata ( from: Repo ( id: repoId) , matching: globs)
351
+ }
352
+
353
+ static func getFileMetadata( from repo: Repo , matching glob: String ) async throws -> [ HubApi . FileMetadata ] {
354
+ return try await HubApi . shared. getFileMetadata ( from: repo, matching: [ glob] )
355
+ }
356
+
357
+ static func getFileMetadata( from repoId: String , matching glob: String ) async throws -> [ HubApi . FileMetadata ] {
358
+ return try await HubApi . shared. getFileMetadata ( from: Repo ( id: repoId) , matching: [ glob] )
359
+ }
262
360
}
263
361
264
362
public extension [ String ] {
0 commit comments