@@ -11,6 +11,7 @@ import Foundation
11
11
12
12
class Downloader : NSObject , ObservableObject {
13
13
private( set) var destination : URL
14
+ private( set) var sourceURL : URL
14
15
15
16
private let chunkSize = 10 * 1024 * 1024 // 10MB
16
17
@@ -30,18 +31,28 @@ class Downloader: NSObject, ObservableObject {
30
31
private( set) lazy var downloadState : CurrentValueSubject < DownloadState , Never > = CurrentValueSubject ( . notStarted)
31
32
private var stateSubscriber : Cancellable ?
32
33
33
- private( set) var tempFilePath : URL
34
+ private( set) var tempFilePath : URL ?
34
35
private( set) var expectedSize : Int ?
35
36
private( set) var downloadedSize : Int = 0
36
37
37
38
private var urlSession : URLSession ? = nil
38
39
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
+
39
46
/// Check if an incomplete file exists for the destination and returns its size
40
47
/// - Parameter destination: The destination URL for the download
41
48
/// - 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
+
43
52
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
+ {
45
56
return fileSize
46
57
}
47
58
}
@@ -52,22 +63,29 @@ class Downloader: NSObject, ObservableObject {
52
63
init (
53
64
from url: URL ,
54
65
to destination: URL ,
55
- incompleteDestination: URL ,
56
66
using authToken: String ? = nil ,
57
67
inBackground: Bool = false ,
68
+ resumeSize: Int = 0 , // Can be specified manually, but will also check for incomplete files
58
69
headers: [ String : String ] ? = nil ,
59
70
expectedSize: Int ? = nil ,
60
71
timeout: TimeInterval = 10 ,
61
72
numRetries: Int = 5
62
73
) {
63
74
self . destination = destination
75
+ sourceURL = url
64
76
self . expectedSize = expectedSize
65
77
66
78
// Create incomplete file path based on destination
67
- self . tempFilePath = incompleteDestination
79
+ tempFilePath = Downloader . incompletePath ( for : destination )
68
80
69
81
// 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
71
89
72
90
super. init ( )
73
91
let sessionIdentifier = " swift-transformers.hub.downloader "
@@ -81,7 +99,7 @@ class Downloader: NSObject, ObservableObject {
81
99
82
100
urlSession = URLSession ( configuration: config, delegate: self , delegateQueue: nil )
83
101
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)
85
103
}
86
104
87
105
/// Sets up and initiates a file download operation
@@ -121,6 +139,25 @@ class Downloader: NSObject, ObservableObject {
121
139
122
140
Task {
123
141
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
+
124
161
// Set up the request with appropriate headers
125
162
var request = URLRequest ( url: url)
126
163
var requestHeaders = headers ?? [ : ]
@@ -130,12 +167,12 @@ class Downloader: NSObject, ObservableObject {
130
167
}
131
168
132
169
// 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 ) - "
135
172
136
173
// Calculate and show initial progress
137
174
if let expectedSize, expectedSize > 0 {
138
- let initialProgress = Double ( resumeSize ) / Double( expectedSize)
175
+ let initialProgress = Double ( existingSize ) / Double( expectedSize)
139
176
self . downloadState. value = . downloading( initialProgress)
140
177
} else {
141
178
self . downloadState. value = . downloading( 0 )
@@ -148,10 +185,10 @@ class Downloader: NSObject, ObservableObject {
148
185
request. allHTTPHeaderFields = requestHeaders
149
186
150
187
// Open the incomplete file for writing
151
- let tempFile = try FileHandle ( forWritingTo: self . tempFilePath )
188
+ let tempFile = try FileHandle ( forWritingTo: incompleteFilePath )
152
189
153
190
// If resuming, seek to end of file
154
- if resumeSize > 0 {
191
+ if existingSize > 0 {
155
192
try tempFile. seekToEnd ( )
156
193
}
157
194
@@ -160,7 +197,7 @@ class Downloader: NSObject, ObservableObject {
160
197
161
198
// Clean up and move the completed download to its final destination
162
199
tempFile. closeFile ( )
163
- try FileManager . default . moveDownloadedFile ( from: self . tempFilePath , to: self . destination)
200
+ try fileManager . moveDownloadedFile ( from: incompleteFilePath , to: self . destination)
164
201
self . downloadState. value = . completed( self . destination)
165
202
} catch {
166
203
self . downloadState. value = . failed( error)
0 commit comments