Skip to content

Commit 92303cb

Browse files
committed
Revert "Merge pull request #1 from ardaatahan/improve-resumable-downloads"
This reverts commit 94032f4, reversing changes made to ba49d2f.
1 parent 34ccd41 commit 92303cb

File tree

4 files changed

+90
-122
lines changed

4 files changed

+90
-122
lines changed

Sources/Hub/Downloader.swift

+50-13
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Foundation
1111

1212
class Downloader: NSObject, ObservableObject {
1313
private(set) var destination: URL
14+
private(set) var sourceURL: URL
1415

1516
private let chunkSize = 10 * 1024 * 1024 // 10MB
1617

@@ -30,18 +31,28 @@ class Downloader: NSObject, ObservableObject {
3031
private(set) lazy var downloadState: CurrentValueSubject<DownloadState, Never> = CurrentValueSubject(.notStarted)
3132
private var stateSubscriber: Cancellable?
3233

33-
private(set) var tempFilePath: URL
34+
private(set) var tempFilePath: URL?
3435
private(set) var expectedSize: Int?
3536
private(set) var downloadedSize: Int = 0
3637

3738
private var urlSession: URLSession? = nil
3839

40+
/// Creates the incomplete file path for a given destination URL
41+
/// This is similar to the Hugging Face Hub approach of using .incomplete files
42+
static func incompletePath(for destination: URL) -> URL {
43+
destination.appendingPathExtension("incomplete")
44+
}
45+
3946
/// Check if an incomplete file exists for the destination and returns its size
4047
/// - Parameter destination: The destination URL for the download
4148
/// - Returns: Size of the incomplete file if it exists, otherwise 0
42-
static func incompleteFileSize(at incompletePath: URL) -> Int {
49+
static func checkForIncompleteFile(at destination: URL) -> Int {
50+
let incompletePath = Self.incompletePath(for: destination)
51+
4352
if FileManager.default.fileExists(atPath: incompletePath.path) {
44-
if let attributes = try? FileManager.default.attributesOfItem(atPath: incompletePath.path), let fileSize = attributes[.size] as? Int {
53+
if let attributes = try? FileManager.default.attributesOfItem(atPath: incompletePath.path),
54+
let fileSize = attributes[.size] as? Int
55+
{
4556
return fileSize
4657
}
4758
}
@@ -52,22 +63,29 @@ class Downloader: NSObject, ObservableObject {
5263
init(
5364
from url: URL,
5465
to destination: URL,
55-
incompleteDestination: URL,
5666
using authToken: String? = nil,
5767
inBackground: Bool = false,
68+
resumeSize: Int = 0, // Can be specified manually, but will also check for incomplete files
5869
headers: [String: String]? = nil,
5970
expectedSize: Int? = nil,
6071
timeout: TimeInterval = 10,
6172
numRetries: Int = 5
6273
) {
6374
self.destination = destination
75+
sourceURL = url
6476
self.expectedSize = expectedSize
6577

6678
// Create incomplete file path based on destination
67-
self.tempFilePath = incompleteDestination
79+
tempFilePath = Downloader.incompletePath(for: destination)
6880

6981
// If resume size wasn't specified, check for an existing incomplete file
70-
let resumeSize = Self.incompleteFileSize(at: incompleteDestination)
82+
let actualResumeSize: Int = if resumeSize > 0 {
83+
resumeSize
84+
} else {
85+
Downloader.checkForIncompleteFile(at: destination)
86+
}
87+
88+
downloadedSize = actualResumeSize
7189

7290
super.init()
7391
let sessionIdentifier = "swift-transformers.hub.downloader"
@@ -81,7 +99,7 @@ class Downloader: NSObject, ObservableObject {
8199

82100
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
83101

84-
setUpDownload(from: url, with: authToken, resumeSize: resumeSize, headers: headers, expectedSize: expectedSize, timeout: timeout, numRetries: numRetries)
102+
setUpDownload(from: url, with: authToken, resumeSize: actualResumeSize, headers: headers, expectedSize: expectedSize, timeout: timeout, numRetries: numRetries)
85103
}
86104

87105
/// Sets up and initiates a file download operation
@@ -121,6 +139,25 @@ class Downloader: NSObject, ObservableObject {
121139

122140
Task {
123141
do {
142+
// Check if incomplete file exists and get its size
143+
var existingSize = 0
144+
guard let incompleteFilePath = self.tempFilePath else {
145+
throw DownloadError.unexpectedError
146+
}
147+
148+
let fileManager = FileManager.default
149+
if fileManager.fileExists(atPath: incompleteFilePath.path) {
150+
let attributes = try fileManager.attributesOfItem(atPath: incompleteFilePath.path)
151+
existingSize = attributes[.size] as? Int ?? 0
152+
self.downloadedSize = existingSize
153+
} else {
154+
// Create parent directory if needed
155+
try fileManager.createDirectory(at: incompleteFilePath.deletingLastPathComponent(), withIntermediateDirectories: true)
156+
157+
// Create empty incomplete file
158+
fileManager.createFile(atPath: incompleteFilePath.path, contents: nil)
159+
}
160+
124161
// Set up the request with appropriate headers
125162
var request = URLRequest(url: url)
126163
var requestHeaders = headers ?? [:]
@@ -130,12 +167,12 @@ class Downloader: NSObject, ObservableObject {
130167
}
131168

132169
// Set Range header if we're resuming
133-
if resumeSize > 0 {
134-
requestHeaders["Range"] = "bytes=\(resumeSize)-"
170+
if existingSize > 0 {
171+
requestHeaders["Range"] = "bytes=\(existingSize)-"
135172

136173
// Calculate and show initial progress
137174
if let expectedSize, expectedSize > 0 {
138-
let initialProgress = Double(resumeSize) / Double(expectedSize)
175+
let initialProgress = Double(existingSize) / Double(expectedSize)
139176
self.downloadState.value = .downloading(initialProgress)
140177
} else {
141178
self.downloadState.value = .downloading(0)
@@ -148,10 +185,10 @@ class Downloader: NSObject, ObservableObject {
148185
request.allHTTPHeaderFields = requestHeaders
149186

150187
// Open the incomplete file for writing
151-
let tempFile = try FileHandle(forWritingTo: self.tempFilePath)
188+
let tempFile = try FileHandle(forWritingTo: incompleteFilePath)
152189

153190
// If resuming, seek to end of file
154-
if resumeSize > 0 {
191+
if existingSize > 0 {
155192
try tempFile.seekToEnd()
156193
}
157194

@@ -160,7 +197,7 @@ class Downloader: NSObject, ObservableObject {
160197

161198
// Clean up and move the completed download to its final destination
162199
tempFile.closeFile()
163-
try FileManager.default.moveDownloadedFile(from: self.tempFilePath, to: self.destination)
200+
try fileManager.moveDownloadedFile(from: incompleteFilePath, to: self.destination)
164201
self.downloadState.value = .completed(self.destination)
165202
} catch {
166203
self.downloadState.value = .failed(error)

Sources/Hub/HubApi.swift

+14-12
Original file line numberDiff line numberDiff line change
@@ -362,10 +362,6 @@ public extension HubApi {
362362
repoMetadataDestination.appending(path: relativeFilename + ".metadata")
363363
}
364364

365-
var incompleteDestination: URL {
366-
repoMetadataDestination.appending(path: relativeFilename + ".incomplete")
367-
}
368-
369365
var downloaded: Bool {
370366
FileManager.default.fileExists(atPath: destination.path)
371367
}
@@ -375,13 +371,9 @@ public extension HubApi {
375371
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
376372
}
377373

378-
// We're using incomplete destination to prepare cache destination because incomplete files include lfs + non-lfs files (vs only lfs for metadata files)
379-
func prepareCacheDestination() throws {
380-
let directoryURL = incompleteDestination.deletingLastPathComponent()
374+
func prepareMetadataDestination() throws {
375+
let directoryURL = metadataDestination.deletingLastPathComponent()
381376
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
382-
if !FileManager.default.fileExists(atPath: incompleteDestination.path) {
383-
try "".write(to: incompleteDestination, atomically: true, encoding: .utf8)
384-
}
385377
}
386378

387379
/// Note we go from Combine in Downloader to callback-based progress reporting
@@ -432,14 +424,24 @@ public extension HubApi {
432424

433425
// Otherwise, let's download the file!
434426
try prepareDestination()
435-
try prepareCacheDestination()
427+
try prepareMetadataDestination()
436428

429+
// Check for an existing incomplete file
430+
let incompleteFile = Downloader.incompletePath(for: destination)
431+
var resumeSize = 0
432+
433+
if FileManager.default.fileExists(atPath: incompleteFile.path) {
434+
if let fileAttributes = try? FileManager.default.attributesOfItem(atPath: incompleteFile.path) {
435+
resumeSize = (fileAttributes[FileAttributeKey.size] as? Int) ?? 0
436+
}
437+
}
438+
437439
let downloader = Downloader(
438440
from: source,
439441
to: destination,
440-
incompleteDestination: incompleteDestination,
441442
using: hfToken,
442443
inBackground: backgroundSession,
444+
resumeSize: resumeSize,
443445
expectedSize: remoteSize
444446
)
445447

Tests/HubTests/DownloaderTests.swift

+26-23
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,9 @@ final class DownloaderTests: XCTestCase {
5959
6060
"""
6161

62-
let cacheDir = tempDir.appendingPathComponent("cache")
63-
try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
64-
65-
let incompleteDestination = cacheDir.appendingPathComponent("config.json.incomplete")
66-
FileManager.default.createFile(atPath: incompleteDestination.path, contents: nil, attributes: nil)
67-
6862
let downloader = Downloader(
6963
from: url,
70-
to: destination,
71-
incompleteDestination: incompleteDestination
64+
to: destination
7265
)
7366

7467
// Store subscriber outside the continuation to maintain its lifecycle
@@ -102,17 +95,10 @@ final class DownloaderTests: XCTestCase {
10295
let url = URL(string: "https://huggingface.co/coreml-projects/Llama-2-7b-chat-coreml/resolve/main/config.json")!
10396
let destination = tempDir.appendingPathComponent("config.json")
10497

105-
let cacheDir = tempDir.appendingPathComponent("cache")
106-
try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
107-
108-
let incompleteDestination = cacheDir.appendingPathComponent("config.json.incomplete")
109-
FileManager.default.createFile(atPath: incompleteDestination.path, contents: nil, attributes: nil)
110-
11198
// Create downloader with incorrect expected size
11299
let downloader = Downloader(
113100
from: url,
114101
to: destination,
115-
incompleteDestination: incompleteDestination,
116102
expectedSize: 999999 // Incorrect size
117103
)
118104

@@ -134,17 +120,10 @@ final class DownloaderTests: XCTestCase {
134120
// Create parent directory if it doesn't exist
135121
try FileManager.default.createDirectory(at: destination.deletingLastPathComponent(),
136122
withIntermediateDirectories: true)
137-
138-
let cacheDir = tempDir.appendingPathComponent("cache")
139-
try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
140-
141-
let incompleteDestination = cacheDir.appendingPathComponent("config.json.incomplete")
142-
FileManager.default.createFile(atPath: incompleteDestination.path, contents: nil, attributes: nil)
143-
123+
144124
let downloader = Downloader(
145125
from: url,
146126
to: destination,
147-
incompleteDestination: incompleteDestination,
148127
expectedSize: 73194001 // Correct size for verification
149128
)
150129

@@ -189,4 +168,28 @@ final class DownloaderTests: XCTestCase {
189168
throw error
190169
}
191170
}
171+
172+
func testAutomaticIncompleteFileDetection() throws {
173+
let url = URL(string: "https://huggingface.co/coreml-projects/sam-2-studio/resolve/main/SAM%202%20Studio%201.1.zip")!
174+
let destination = tempDir.appendingPathComponent("SAM%202%20Studio%201.1.zip")
175+
176+
// Create a sample incomplete file with test content
177+
let incompletePath = Downloader.incompletePath(for: destination)
178+
try FileManager.default.createDirectory(at: incompletePath.deletingLastPathComponent(), withIntermediateDirectories: true)
179+
let testContent = Data(repeating: 65, count: 1024) // 1KB of data
180+
FileManager.default.createFile(atPath: incompletePath.path, contents: testContent)
181+
182+
// Create a downloader for the same destination
183+
// It should automatically detect and use the incomplete file
184+
let downloader = Downloader(
185+
from: url,
186+
to: destination
187+
)
188+
189+
// Verify the downloader found and is using the incomplete file
190+
XCTAssertEqual(downloader.downloadedSize, 1024, "Should have detected the incomplete file and set resumeSize")
191+
192+
// Clean up
193+
try? FileManager.default.removeItem(at: incompletePath)
194+
}
192195
}

Tests/HubTests/HubApiTests.swift

-74
Original file line numberDiff line numberDiff line change
@@ -968,78 +968,4 @@ class SnapshotDownloadTests: XCTestCase {
968968
XCTFail("Unexpected error: \(error)")
969969
}
970970
}
971-
972-
func testResumeDownloadFromEmptyIncomplete() async throws {
973-
let hubApi = HubApi(downloadBase: downloadDestination)
974-
var lastProgress: Progress? = nil
975-
var downloadedTo = FileManager.default.homeDirectoryForCurrentUser
976-
.appendingPathComponent("Library/Caches/huggingface-tests/models/coreml-projects/Llama-2-7b-chat-coreml")
977-
978-
let metadataDestination = downloadedTo.appending(component: ".cache/huggingface/download")
979-
980-
try FileManager.default.createDirectory(at: metadataDestination, withIntermediateDirectories: true, attributes: nil)
981-
try "".write(to: metadataDestination.appendingPathComponent("config.json.incomplete"), atomically: true, encoding: .utf8)
982-
downloadedTo = try await hubApi.snapshot(from: repo, matching: "config.json") { progress in
983-
print("Total Progress: \(progress.fractionCompleted)")
984-
print("Files Completed: \(progress.completedUnitCount) of \(progress.totalUnitCount)")
985-
lastProgress = progress
986-
}
987-
XCTAssertEqual(lastProgress?.fractionCompleted, 1)
988-
XCTAssertEqual(lastProgress?.completedUnitCount, 1)
989-
XCTAssertEqual(downloadedTo, downloadDestination.appending(path: "models/\(repo)"))
990-
991-
let fileContents = try String(contentsOfFile: downloadedTo.appendingPathComponent("config.json").path)
992-
993-
let expected = """
994-
{
995-
"architectures": [
996-
"LlamaForCausalLM"
997-
],
998-
"bos_token_id": 1,
999-
"eos_token_id": 2,
1000-
"model_type": "llama",
1001-
"pad_token_id": 0,
1002-
"vocab_size": 32000
1003-
}
1004-
1005-
"""
1006-
XCTAssertTrue(fileContents.contains(expected))
1007-
}
1008-
1009-
func testResumeDownloadFromNonEmptyIncomplete() async throws {
1010-
let hubApi = HubApi(downloadBase: downloadDestination)
1011-
var lastProgress: Progress? = nil
1012-
var downloadedTo = FileManager.default.homeDirectoryForCurrentUser
1013-
.appendingPathComponent("Library/Caches/huggingface-tests/models/coreml-projects/Llama-2-7b-chat-coreml")
1014-
1015-
let metadataDestination = downloadedTo.appending(component: ".cache/huggingface/download")
1016-
1017-
try FileManager.default.createDirectory(at: metadataDestination, withIntermediateDirectories: true, attributes: nil)
1018-
try "X".write(to: metadataDestination.appendingPathComponent("config.json.incomplete"), atomically: true, encoding: .utf8)
1019-
downloadedTo = try await hubApi.snapshot(from: repo, matching: "config.json") { progress in
1020-
print("Total Progress: \(progress.fractionCompleted)")
1021-
print("Files Completed: \(progress.completedUnitCount) of \(progress.totalUnitCount)")
1022-
lastProgress = progress
1023-
}
1024-
XCTAssertEqual(lastProgress?.fractionCompleted, 1)
1025-
XCTAssertEqual(lastProgress?.completedUnitCount, 1)
1026-
XCTAssertEqual(downloadedTo, downloadDestination.appending(path: "models/\(repo)"))
1027-
1028-
let fileContents = try String(contentsOfFile: downloadedTo.appendingPathComponent("config.json").path)
1029-
1030-
let expected = """
1031-
X
1032-
"architectures": [
1033-
"LlamaForCausalLM"
1034-
],
1035-
"bos_token_id": 1,
1036-
"eos_token_id": 2,
1037-
"model_type": "llama",
1038-
"pad_token_id": 0,
1039-
"vocab_size": 32000
1040-
}
1041-
1042-
"""
1043-
XCTAssertTrue(fileContents.contains(expected))
1044-
}
1045971
}

0 commit comments

Comments
 (0)