Skip to content

Commit 654b68a

Browse files
committed
Use MuxUpload id instead if the input URL when looking up or writing state in the SDK (#33)
Keep MuxUpload id immutable Avoid duplicating the id across MuxUpload and UploadInfo Use XCTUnwrap to read persistence entries in tests Add a no throw check when writing persistence entries in tests Use XCTUnwrap when reading all Expose init method internally to rest of SDK module for testing Attempt to add an upload dedupe test Use XCTUnwrap for constructing test URLs Expose UploadManager init to rest of module for testing Use the public init
1 parent cb2a4d7 commit 654b68a

File tree

6 files changed

+178
-51
lines changed

6 files changed

+178
-51
lines changed

Sources/MuxUploadSDK/PublicAPI/MuxUpload.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ public final class MuxUpload : Hashable, Equatable {
4848

4949
private let uploadInfo: UploadInfo
5050
private let manageBySDK: Bool
51-
private let id: String = UUID().uuidString
51+
private var id: String {
52+
uploadInfo.id
53+
}
5254
private let uploadManager: UploadManager
5355

5456
private var lastSeenStatus: Status = Status(progress: Progress(totalUnitCount: 0), updatedTime: 0, startTime: 0, isPaused: false)
@@ -96,6 +98,7 @@ public final class MuxUpload : Hashable, Equatable {
9698
optOutOfEventTracking: Bool = false
9799
) {
98100
let uploadInfo = UploadInfo(
101+
id: UUID().uuidString,
99102
uploadURL: uploadURL,
100103
videoFile: videoFileURL,
101104
chunkSize: chunkSize,
@@ -209,7 +212,7 @@ public final class MuxUpload : Hashable, Equatable {
209212
*/
210213
public func cancel() {
211214
fileWorker?.cancel()
212-
uploadManager.acknowledgeUpload(ofFile: videoFile)
215+
uploadManager.acknowledgeUpload(id: id)
213216

214217
lastSeenStatus = Status(progress: nil, updatedTime: 0, startTime: 0, isPaused: true)
215218
progressHandler = nil
@@ -273,14 +276,22 @@ public final class MuxUpload : Hashable, Equatable {
273276
}
274277

275278

276-
private init (uploadInfo: UploadInfo, manage: Bool = true, uploadManager: UploadManager) {
279+
internal init (
280+
uploadInfo: UploadInfo,
281+
manage: Bool = true,
282+
uploadManager: UploadManager
283+
) {
277284
self.uploadInfo = uploadInfo
278285
self.manageBySDK = manage
279286
self.uploadManager = uploadManager
280287
}
281288

282289
internal convenience init(wrapping uploader: ChunkedFileUploader, uploadManager: UploadManager) {
283-
self.init(uploadInfo: uploader.uploadInfo, manage: true, uploadManager: uploadManager)
290+
self.init(
291+
uploadInfo: uploader.uploadInfo,
292+
manage: true,
293+
uploadManager: uploadManager
294+
)
284295
self.fileWorker = uploader
285296

286297
handleStateUpdate(uploader.currentState)

Sources/MuxUploadSDK/PublicAPI/UploadManager.swift

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import Foundation
2828
///
2929
public final class UploadManager {
3030

31-
private var uploadersByURL: [URL : ChunkedFileUploader] = [:]
31+
private var uploadersByID: [String : ChunkedFileUploader] = [:]
3232
private var uploadsUpdateDelegatesByToken: [ObjectIdentifier : any UploadsUpdatedDelegate] = [:]
3333
private let uploadActor = UploadCacheActor()
3434
private lazy var uploaderDelegate: FileUploaderDelegate = FileUploaderDelegate(manager: self)
@@ -37,7 +37,12 @@ public final class UploadManager {
3737
/// to track and control its state
3838
/// Returns nil if there was no uplod in progress for thr given file
3939
public func findStartedUpload(ofFile url: URL) -> MuxUpload? {
40-
if let uploader = uploadersByURL[url] {
40+
if let uploader = Dictionary<URL, ChunkedFileUploader>(
41+
uniqueKeysWithValues: uploadersByID.mapValues { value in
42+
(value.uploadInfo.videoFile, value)
43+
}
44+
.values
45+
)[url] {
4146
return MuxUpload(wrapping: uploader, uploadManager: self)
4247
} else {
4348
return nil
@@ -48,7 +53,7 @@ public final class UploadManager {
4853
/// Uploads are managed while in-progress or compelted.
4954
/// Uploads become un-managed when canceled, or if the process dies after they complete
5055
public func allManagedUploads() -> [MuxUpload] {
51-
return uploadersByURL.compactMap { (key, value) in MuxUpload(wrapping: value, uploadManager: self) }
56+
return uploadersByID.compactMap { (key, value) in MuxUpload(wrapping: value, uploadManager: self) }
5257
}
5358

5459
/// Attempts to resume an upload that was previously paused or interrupted by process death
@@ -94,23 +99,28 @@ public final class UploadManager {
9499
uploadsUpdateDelegatesByToken.removeValue(forKey: ObjectIdentifier(delegate))
95100
}
96101

97-
internal func acknowledgeUpload(ofFile url: URL) {
98-
if let uploader = uploadersByURL[url] {
102+
internal func acknowledgeUpload(id: String) {
103+
if let uploader = uploadersByID[id] {
99104
uploader.cancel()
100105
}
101-
uploadersByURL.removeValue(forKey: url)
106+
uploadersByID.removeValue(forKey: id)
102107
Task.detached {
103-
await self.uploadActor.remove(uploadAt:url)
108+
await self.uploadActor.remove(uploadID: id)
104109
self.notifyDelegates()
105110
}
106111
}
107112

108113
internal func findUploaderFor(videoFile url: URL) -> ChunkedFileUploader? {
109-
return uploadersByURL[url]
114+
return Dictionary<URL, ChunkedFileUploader>(
115+
uniqueKeysWithValues: uploadersByID.mapValues { value in
116+
(value.uploadInfo.videoFile, value)
117+
}
118+
.values
119+
)[url]
110120
}
111121

112122
internal func registerUploader(_ fileWorker: ChunkedFileUploader, withId id: String) {
113-
uploadersByURL.updateValue(fileWorker, forKey: fileWorker.uploadInfo.videoFile)
123+
uploadersByID.updateValue(fileWorker, forKey: fileWorker.uploadInfo.id)
114124
fileWorker.addDelegate(withToken: UUID().uuidString, uploaderDelegate)
115125
Task.detached {
116126
await self.uploadActor.updateUpload(
@@ -134,7 +144,7 @@ public final class UploadManager {
134144

135145
/// The shared instance of this object that should be used
136146
public static let shared = UploadManager()
137-
private init() { }
147+
internal init() { }
138148

139149
private struct FileUploaderDelegate : ChunkedFileUploaderDelegate {
140150
let manager: UploadManager
@@ -145,7 +155,7 @@ public final class UploadManager {
145155
manager.notifyDelegates()
146156
}
147157
switch state {
148-
case .success(_), .canceled: manager.acknowledgeUpload(ofFile: uploader.uploadInfo.videoFile)
158+
case .success(_), .canceled: manager.acknowledgeUpload(id: uploader.uploadInfo.id)
149159
default: do { }
150160
}
151161
}
@@ -169,16 +179,31 @@ internal actor UploadCacheActor {
169179
}
170180
}
171181

172-
func getUpload(ofFileAt url: URL) async -> ChunkedFileUploader? {
182+
func getUpload(uploadID: String) async -> ChunkedFileUploader? {
173183
// reminder: doesn't start the uploader, just makes it
174184
return await Task<ChunkedFileUploader?, Never> {
175-
let optEntry = try? persistence.readEntry(forFileAt: url)
185+
let optEntry = try? persistence.readEntry(uploadID: uploadID)
176186
guard let entry = optEntry else {
177187
return nil
178188
}
179189
return ChunkedFileUploader(uploadInfo: entry.uploadInfo, startingAtByte: entry.lastSuccessfulByte)
180190
}.value
181191
}
192+
193+
func getUpload(ofFileAt: URL) async -> ChunkedFileUploader? {
194+
return await Task<ChunkedFileUploader?, Never> {
195+
guard let matchingEntry = try? persistence.readAll().filter({
196+
$0.uploadInfo.uploadURL == ofFileAt
197+
}).first else {
198+
return nil
199+
}
200+
201+
return ChunkedFileUploader(
202+
uploadInfo: matchingEntry.uploadInfo,
203+
startingAtByte: matchingEntry.lastSuccessfulByte
204+
)
205+
}.value
206+
}
182207

183208
func getAllUploads() async -> [ChunkedFileUploader] {
184209
return await Task<[ChunkedFileUploader]?, Never> {
@@ -188,9 +213,9 @@ internal actor UploadCacheActor {
188213
}.value ?? []
189214
}
190215

191-
func remove(uploadAt url: URL) async {
216+
func remove(uploadID: String) async {
192217
await Task<(), Never> {
193-
try? persistence.remove(entryAtAbsUrl: url)
218+
try? persistence.remove(entryAtID: uploadID)
194219
}.value
195220
}
196221

Sources/MuxUploadSDK/Upload/UploadInfo.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import Foundation
1111
Internal representation of a video upload
1212
*/
1313
struct UploadInfo : Codable {
14+
15+
var id: String
1416
/**
1517
URI of the upload's destinatoin
1618
*/

Sources/MuxUploadSDK/Upload/UploadPersistence.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ class UploadPersistence {
1515
private static let ENTRY_TTL: TimeInterval = 3 * 24 * 60 * 60 // 3 days
1616

1717
private let fileURL: URL
18-
private var cache: [URL : PersistenceEntry]? // populated on first write for this object (see ensureCache())
18+
private var cache: [String : PersistenceEntry]? // populated on first write for this object (see ensureCache())
1919
private let uploadsFile: UploadsFile
2020

2121
func update(uploadState state: ChunkedFileUploader.InternalUploadState, forUpload upload: UploadInfo) {
2222
do {
2323
// If the new state is persistable, persist it (overwriting the old) otherwise delete it
2424
if let entry = PersistenceEntry.fromUploadState(state, forUpload: upload) {
25-
try write(entry: entry, forFileAt: upload.videoFile)
25+
try write(entry: entry, for: upload.id)
2626
} else {
27-
try remove(entryAtAbsUrl: upload.uploadURL)
27+
try remove(entryAtID: upload.id)
2828
}
2929
} catch {
3030
// This makes a lot of noise on the emulator, but might be worth logging if you're having issues around here
@@ -46,10 +46,10 @@ class UploadPersistence {
4646
}
4747
}
4848

49-
func remove(entryAtAbsUrl url: URL) throws {
49+
func remove(entryAtID id: String) throws {
5050
try maybeOpenCache()
5151
if var cache = cache {
52-
cache.removeValue(forKey: url)
52+
cache.removeValue(forKey: id)
5353
self.cache = cache
5454
// write-through
5555
try uploadsFile.writeContents(of: UploadsFileContents(mapOf: cache))
@@ -58,10 +58,10 @@ class UploadPersistence {
5858

5959
/// Directly writes entry. Updates are written-through the internal cache to the backing file
6060
/// This method does I/O
61-
func write(entry: PersistenceEntry, forFileAt fileUrl: URL) throws {
61+
func write(entry: PersistenceEntry, for uploadID: String) throws {
6262
try maybeOpenCache()
6363
if var cache = cache {
64-
cache.updateValue(entry, forKey: fileUrl)
64+
cache.updateValue(entry, forKey: uploadID)
6565
self.cache = cache
6666
try uploadsFile.writeContents(
6767
of: UploadsFileContents(entries: cache.map { (key, value) in value })
@@ -71,10 +71,10 @@ class UploadPersistence {
7171
}
7272

7373
/// Directly reads a single entry based on the file URL given
74-
func readEntry(forFileAt fileUrl: URL) throws -> PersistenceEntry? {
74+
func readEntry(uploadID: String) throws -> PersistenceEntry? {
7575
try maybeOpenCache()
7676
if let cache = cache {
77-
return cache[fileUrl.absoluteURL]
77+
return cache[uploadID]
7878
} else {
7979
return nil
8080
}
@@ -93,7 +93,7 @@ class UploadPersistence {
9393
let nowish = Date().timeIntervalSince1970
9494
for entry in allEntries {
9595
if (nowish - entry.savedAt) > UploadPersistence.ENTRY_TTL {
96-
try remove(entryAtAbsUrl: entry.uploadInfo.videoFile)
96+
try remove(entryAtID: entry.uploadInfo.id)
9797
}
9898
}
9999
}
@@ -182,17 +182,17 @@ protocol UploadsFile {
182182
struct UploadsFileContents : Codable {
183183
let entriesAbsFileUrlToUploadInfo: [PersistenceEntry]
184184

185-
func asDictionary() -> [URL : PersistenceEntry] {
185+
func asDictionary() -> [String : PersistenceEntry] {
186186
return entriesAbsFileUrlToUploadInfo.reduce(into: [:]) { (map, ent) -> () in
187-
map.updateValue(ent, forKey: ent.uploadInfo.videoFile)
187+
map.updateValue(ent, forKey: ent.uploadInfo.id)
188188
}
189189
}
190190

191191
init(entries: [PersistenceEntry]) {
192192
self.entriesAbsFileUrlToUploadInfo = entries
193193
}
194194

195-
init(mapOf items: [URL : PersistenceEntry]) {
195+
init(mapOf items: [String : PersistenceEntry]) {
196196
self.entriesAbsFileUrlToUploadInfo = items.compactMap({ (key, value) in value })
197197
}
198198
}

0 commit comments

Comments
 (0)