Skip to content

Commit 9533b02

Browse files
authored
Merge pull request #209 from tus/dw/memory-usage
Decrease memory usage when resuming an upload
2 parents f8123a6 + fa2a9af commit 9533b02

File tree

7 files changed

+148
-15
lines changed

7 files changed

+148
-15
lines changed

Sources/TUSKit/Files.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,38 @@ final class Files {
157157
}
158158
}
159159

160+
@available(iOS 13.4, macOS 10.15.4, *)
161+
func streamingData(_ dataGenerator: () -> Data?, id: UUID, preferredFileExtension: String? = nil) throws -> URL {
162+
try queue.sync {
163+
try makeDirectoryIfNeeded()
164+
165+
let fileName: String
166+
if let fileExtension = preferredFileExtension {
167+
fileName = id.uuidString + fileExtension
168+
} else {
169+
fileName = id.uuidString
170+
}
171+
172+
let targetLocation = storageDirectory.appendingPathComponent(fileName)
173+
if !FileManager.default.fileExists(atPath: targetLocation.path) {
174+
FileManager.default.createFile(atPath: targetLocation.path, contents: nil)
175+
}
176+
177+
let destinationHandle = try FileHandle(forWritingTo: targetLocation)
178+
try destinationHandle.truncate(atOffset: 0)
179+
defer {
180+
try? destinationHandle.close()
181+
}
182+
183+
while let data = dataGenerator() {
184+
guard !data.isEmpty else { throw FilesError.dataIsEmpty }
185+
try destinationHandle.write(contentsOf: data)
186+
}
187+
188+
return targetLocation
189+
}
190+
}
191+
160192
/// Removes metadata and its related file from disk
161193
/// - Parameter metaData: The metadata description
162194
/// - Throws: Any error from FileManager when removing a file.

Sources/TUSKit/TUSAPI.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@ public enum TUSAPIError: Error {
1515
case couldNotRetrieveOffset
1616
case couldNotRetrieveLocation
1717
case failedRequest(HTTPURLResponse)
18+
19+
public var localizedDescription: String {
20+
switch self {
21+
case .underlyingError(let error):
22+
return "Underlying error: " + error.localizedDescription
23+
case .couldNotFetchStatus:
24+
return "Could not fetch status from server."
25+
case .couldNotFetchServerInfo:
26+
return "Could not fetch server info."
27+
case .couldNotRetrieveOffset:
28+
return "Could not retrieve offset from response."
29+
case .couldNotRetrieveLocation:
30+
return "Could not retrieve location from response."
31+
case .failedRequest(let response):
32+
return "Failed request with status code \(response.statusCode)."
33+
}
34+
}
1835
}
1936

2037
/// The status of an upload.

Sources/TUSKit/TUSClient.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,21 @@ public final class TUSClient {
402402
@discardableResult
403403
public func resume(id: UUID) throws -> Bool {
404404
do {
405-
var uploadMetadata: UploadMetadata?
405+
var metaData: UploadMetadata?
406406
queue.sync {
407-
uploadMetadata = uploads[id]
407+
metaData = uploads[id]
408408
}
409-
guard uploadMetadata != nil else { return false }
410-
guard let metaData = try files.findMetadata(id: id) else {
409+
410+
if metaData == nil {
411+
guard let storedMetadata = try files.findMetadata(id: id) else {
412+
return false
413+
}
414+
415+
metaData = storedMetadata
416+
}
417+
418+
guard let metaData else {
419+
// should never happen...
411420
return false
412421
}
413422

Sources/TUSKit/TUSClientError.swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,53 @@ public enum TUSClientError: Error {
1919
case couldnotRemoveFinishedUploads(underlyingError: Error)
2020
case receivedUnexpectedOffset
2121
case missingRemoteDestination
22-
case emptyUploadRange
2322
case rangeLargerThanFile
2423
case taskCancelled
2524
case customURLSessionWithBackgroundConfigurationNotSupported
25+
case emptyUploadRange
26+
27+
public var localizedDescription: String {
28+
switch self {
29+
case .couldNotCopyFile(let underlyingError):
30+
return "Could not copy file: \(underlyingError.localizedDescription)"
31+
case .couldNotStoreFile(let underlyingError):
32+
return "Could not store file: \(underlyingError.localizedDescription)"
33+
case .fileSizeUnknown:
34+
return "The file size is unknown."
35+
case .couldNotLoadData(let underlyingError):
36+
return "Could not load data: \(underlyingError.localizedDescription)"
37+
case .couldNotStoreFileMetadata(let underlyingError):
38+
return "Could not store file metadata: \(underlyingError.localizedDescription)"
39+
case .couldNotCreateFileOnServer:
40+
return "Could not create file on server."
41+
case .couldNotUploadFile(let underlyingError):
42+
return "Could not upload file: \(underlyingError.localizedDescription)"
43+
case .couldNotGetFileStatus:
44+
return "Could not get file status."
45+
case .fileSizeMismatchWithServer:
46+
return "File size mismatch with server."
47+
case .couldNotDeleteFile(let underlyingError):
48+
return "Could not delete file: \(underlyingError.localizedDescription)"
49+
case .uploadIsAlreadyFinished:
50+
return "The upload is already finished."
51+
case .couldNotRetryUpload:
52+
return "Could not retry upload."
53+
case .couldNotResumeUpload:
54+
return "Could not resume upload."
55+
case .couldnotRemoveFinishedUploads(let underlyingError):
56+
return "Could not remove finished uploads: \(underlyingError.localizedDescription)"
57+
case .receivedUnexpectedOffset:
58+
return "Received unexpected offset."
59+
case .missingRemoteDestination:
60+
return "Missing remote destination for upload."
61+
case .emptyUploadRange:
62+
return "The upload range is empty."
63+
case .rangeLargerThanFile:
64+
return "The upload range is larger than the file size."
65+
case .taskCancelled:
66+
return "The task was cancelled."
67+
case .customURLSessionWithBackgroundConfigurationNotSupported:
68+
return "Custom URLSession with background configuration is not supported."
69+
}
70+
}
2671
}

Sources/TUSKit/Tasks/UploadDataTask.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,25 @@ final class UploadDataTask: NSObject, IdentifiableTask {
182182

183183
// Can't use switch with #available :'(
184184
let data: Data
185-
if let range = self.range, #available(iOS 13.0, macOS 10.15, *) { // Has range, for newer versions
186-
try fileHandle.seek(toOffset: UInt64(range.startIndex))
187-
data = fileHandle.readData(ofLength: range.count)
185+
if let range = self.range, #available(iOS 13.4, macOS 10.15.4, *) { // Has range, for newer versions
186+
var offset = range.startIndex
187+
188+
return try files.streamingData({
189+
autoreleasepool {
190+
do {
191+
let chunkSize = min(1024 * 1024 * 500, range.endIndex - offset)
192+
try fileHandle.seek(toOffset: UInt64(offset))
193+
guard offset < range.endIndex else { return nil }
194+
195+
let data = fileHandle.readData(ofLength: chunkSize)
196+
print("read data of size \(data.count) at offset \(offset)")
197+
offset += chunkSize
198+
return data
199+
} catch {
200+
return nil
201+
}
202+
}
203+
}, id: metaData.id, preferredFileExtension: "uploadData")
188204
} else if let range = self.range { // Has range, for older versions
189205
fileHandle.seek(toFileOffset: UInt64(range.startIndex))
190206
data = fileHandle.readData(ofLength: range.count)

TUSKitExample/TUSKitExample/Helpers/TUSWrapper.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,20 @@ class TUSWrapper: ObservableObject {
4040

4141
@MainActor
4242
func resumeUpload(id: UUID) {
43-
_ = try? client.resume(id: id)
44-
45-
if case let .paused(bytesUploaded, totalBytes) = uploads[id] {
46-
withAnimation {
47-
uploads[id] = .uploading(bytesUploaded: bytesUploaded, totalBytes: totalBytes)
43+
do {
44+
guard try client.resume(id: id) == true else {
45+
print("Upload not resumed; metadata not found")
46+
return
4847
}
48+
49+
if case let .paused(bytesUploaded, totalBytes) = uploads[id] {
50+
withAnimation {
51+
uploads[id] = .uploading(bytesUploaded: bytesUploaded, totalBytes: totalBytes)
52+
}
53+
}
54+
} catch {
55+
print("Could not resume upload with id \(id)")
56+
print(error)
4957
}
5058
}
5159

@@ -99,6 +107,10 @@ extension TUSWrapper: TUSClientDelegate {
99107

100108
func uploadFailed(id: UUID, error: Error, context: [String : String]?, client: TUSClient) {
101109
Task { @MainActor in
110+
// Pausing an upload means we cancel it, so we don't want to show it as failed.
111+
if let tusError = error as? TUSClientError, case .taskCancelled = tusError {
112+
return
113+
}
102114

103115
withAnimation {
104116
uploads[id] = .failed(error: error)

TUSKitExample/TUSKitExample/Screens/Upload/UploadsListView.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,6 @@ extension UploadsListView {
117117

118118

119119
// MARK: - Records List View
120-
121-
122120
@ViewBuilder
123121
private func uploadRecordsListView(items: [UUID: UploadStatus]) -> some View {
124122
ScrollView {
@@ -135,6 +133,10 @@ extension UploadsListView {
135133
UploadedRowView(key: idx.key, url: url)
136134
case .failed(let error):
137135
FailedRowView(key: idx.key, error: error)
136+
.onAppear {
137+
print(error)
138+
print(error.localizedDescription)
139+
}
138140
}
139141
}
140142
Divider()

0 commit comments

Comments
 (0)