Skip to content

Commit 60b4832

Browse files
authored
Merge pull request #219 from cryptomator/feature/recoverable-uploads
Feature: recoverable uploads
2 parents afb31c6 + 7ab2d83 commit 60b4832

File tree

49 files changed

+919
-172
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+919
-172
lines changed

Cryptomator.xcodeproj/project.pbxproj

Lines changed: 40 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// UploadRetrying.swift
3+
// CryptomatorCommonCore
4+
//
5+
// Created by Philipp Schmid on 03.05.22.
6+
// Copyright © 2021 Skymatic GmbH. All rights reserved.
7+
//
8+
9+
import FileProvider
10+
import Foundation
11+
12+
@objc public protocol UploadRetrying: NSFileProviderServiceSource {
13+
/**
14+
Retries the upload for the given item identifiers.
15+
*/
16+
func retryUpload(for itemIdentifiers: [NSFileProviderItemIdentifier], reply: @escaping (Error?) -> Void)
17+
18+
func getCurrentFractionalUploadProgress(for itemIdentifier: NSFileProviderItemIdentifier, reply: @escaping (NSNumber?) -> Void)
19+
}
20+
21+
public extension NSFileProviderServiceName {
22+
static let uploadRetryingService = NSFileProviderServiceName("org.cryptomator.ios.upload-retrying")
23+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// FileProviderAction.swift
3+
// CryptomatorFileProvider
4+
//
5+
// Created by Philipp Schmid on 03.05.22.
6+
// Copyright © 2022 Skymatic GmbH. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public enum FileProviderAction: String {
12+
case retryUpload = "org.cryptomator.ios.fileprovider.retryUpload"
13+
case retryWaitingUpload = "org.cryptomator.ios.fileprovider.retryWaitingUpload"
14+
}

CryptomatorFileProvider/DB/UploadTaskDBManager.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ extension UploadTaskManager {
4444
}
4545
try removeTaskRecord(for: id)
4646
}
47+
48+
func updateTaskRecord(for itemMetadata: ItemMetadata, with error: NSError) throws {
49+
guard let id = itemMetadata.id else {
50+
throw DBManagerError.nonSavedItemMetadata
51+
}
52+
try updateTaskRecord(with: id, lastFailedUploadDate: Date(), uploadErrorCode: error.code, uploadErrorDomain: error.domain)
53+
}
4754
}
4855

4956
class UploadTaskDBManager: UploadTaskManager {

CryptomatorFileProvider/DB/WorkingSetObserver.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ class WorkingSetObserver: WorkingSetObserving {
2222
private let cachedFileManager: CachedFileManager
2323
private let notificator: FileProviderNotificatorType
2424
private var currentWorkingSetItems = Set<FileProviderItem>()
25+
private let domainIdentifier: NSFileProviderDomainIdentifier
2526

26-
init(database: DatabaseReader, notificator: FileProviderNotificatorType, uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager) {
27+
init(domainIdentifier: NSFileProviderDomainIdentifier, database: DatabaseReader, notificator: FileProviderNotificatorType, uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager) {
28+
self.domainIdentifier = domainIdentifier
2729
self.database = database
2830
self.notificator = notificator
2931
self.uploadTaskManager = uploadTaskManager
@@ -72,7 +74,7 @@ class WorkingSetObserver: WorkingSetObserving {
7274
let localCachedFileInfo = try cachedFileManager.getLocalCachedFileInfo(for: metadata)
7375
let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: metadata.lastModifiedDate) ?? false
7476
let localURL = localCachedFileInfo?.localURL
75-
return FileProviderItem(metadata: metadata, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTasks[index]?.failedWithError)
77+
return FileProviderItem(metadata: metadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTasks[index]?.failedWithError)
7678
}
7779
return items
7880
}

CryptomatorFileProvider/FileProviderAdapter.swift

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public protocol FileProviderAdapterType: AnyObject {
2727
func startProvidingItem(at url: URL, completionHandler: @escaping ((_ error: Error?) -> Void))
2828
func setFavoriteRank(_ favoriteRank: NSNumber?, forItemIdentifier itemIdentifier: NSFileProviderItemIdentifier, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void)
2929
func setTagData(_ tagData: Data?, forItemIdentifier itemIdentifier: NSFileProviderItemIdentifier, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void)
30+
func retryUpload(for itemIdentifier: NSFileProviderItemIdentifier)
3031
}
3132

3233
public class FileProviderAdapter: FileProviderAdapterType {
@@ -44,9 +45,11 @@ public class FileProviderAdapter: FileProviderAdapterType {
4445
private let notificator: FileProviderItemUpdateDelegate?
4546
private let fullVersionChecker: FullVersionChecker
4647
private let workflowFactory: WorkflowFactoryLocking
48+
private let domainIdentifier: NSFileProviderDomainIdentifier
4749

48-
init(uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, reparentTaskManager: ReparentTaskManager, deletionTaskManager: DeletionTaskManager, itemEnumerationTaskManager: ItemEnumerationTaskManager, downloadTaskManager: DownloadTaskManager, scheduler: WorkflowScheduler, provider: CloudProvider, notificator: FileProviderItemUpdateDelegate? = nil, localURLProvider: LocalURLProviderType, fullVersionChecker: FullVersionChecker = UserDefaultsFullVersionChecker.shared) {
50+
init(domainIdentifier: NSFileProviderDomainIdentifier, uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, reparentTaskManager: ReparentTaskManager, deletionTaskManager: DeletionTaskManager, itemEnumerationTaskManager: ItemEnumerationTaskManager, downloadTaskManager: DownloadTaskManager, scheduler: WorkflowScheduler, provider: CloudProvider, notificator: FileProviderItemUpdateDelegate? = nil, localURLProvider: LocalURLProviderType, fullVersionChecker: FullVersionChecker = UserDefaultsFullVersionChecker.shared) {
4951
self.lastUnlockedDate = Date()
52+
self.domainIdentifier = domainIdentifier
5053
self.uploadTaskManager = uploadTaskManager
5154
self.cachedFileManager = cachedFileManager
5255
self.itemMetadataManager = itemMetadataManager
@@ -61,7 +64,8 @@ public class FileProviderAdapter: FileProviderAdapterType {
6164
reparentTaskManager: reparentTaskManager,
6265
deletionTaskManager: deletionTaskManager,
6366
itemEnumerationTaskManager: itemEnumerationTaskManager,
64-
downloadTaskManager: downloadTaskManager)
67+
downloadTaskManager: downloadTaskManager,
68+
domainIdentifier: domainIdentifier)
6569
self.workflowFactory = WorkflowFactoryLocking(lockManager: LockManager(), workflowFactory: factory)
6670
self.scheduler = scheduler
6771
self.provider = provider
@@ -84,7 +88,7 @@ public class FileProviderAdapter: FileProviderAdapterType {
8488
let localCachedFileInfo = try cachedFileManager.getLocalCachedFileInfo(for: itemMetadata)
8589
let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: itemMetadata.lastModifiedDate) ?? false
8690
let localURL = localCachedFileInfo?.localURL
87-
return FileProviderItem(metadata: itemMetadata, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTask?.failedWithError)
91+
return FileProviderItem(metadata: itemMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTask?.failedWithError)
8892
}
8993

9094
// MARK: Enumerate Item
@@ -140,7 +144,7 @@ public class FileProviderAdapter: FileProviderAdapterType {
140144
let localCachedFileInfo = try self.cachedFileManager.getLocalCachedFileInfo(for: metadata)
141145
let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: metadata.lastModifiedDate) ?? false
142146
let localURL = localCachedFileInfo?.localURL
143-
return FileProviderItem(metadata: metadata, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTasks[index]?.failedWithError)
147+
return FileProviderItem(metadata: metadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: uploadTasks[index]?.failedWithError)
144148
}
145149
} catch {
146150
return Promise(error)
@@ -218,7 +222,7 @@ public class FileProviderAdapter: FileProviderAdapterType {
218222

219223
// Register LocalURL in the DB
220224
try cachedFileManager.cacheLocalFileInfo(for: placeholderMetadata.id!, localURL: localURL, lastModifiedDate: nil)
221-
let item = FileProviderItem(metadata: placeholderMetadata, newestVersionLocallyCached: true, localURL: localURL)
225+
let item = FileProviderItem(metadata: placeholderMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: true, localURL: localURL)
222226
let uploadTaskRecord = try registerFileInUploadQueue(with: localURL, itemMetadata: placeholderMetadata)
223227
return LocalItemImportResult(item: item, uploadTaskRecord: uploadTaskRecord)
224228
}
@@ -258,14 +262,39 @@ public class FileProviderAdapter: FileProviderAdapterType {
258262
let itemMetadata = try getCachedMetadata(for: itemIdentifier)
259263
uploadTaskRecord = try registerFileInUploadQueue(with: url, itemMetadata: itemMetadata)
260264
} catch {
261-
DDLogError("itemChanged - failed to register file in upload queue with url: \(url) and identifier: \(itemIdentifier)")
265+
DDLogError("itemChanged - register file in upload queue with url: \(url) and identifier: \(itemIdentifier) failed with error: \(error)")
262266
return
263267
}
264268
uploadFile(taskRecord: uploadTaskRecord).then { item in
265269
self.notificator?.signalUpdate(for: item)
266270
}
267271
}
268272

273+
public func retryUpload(for itemIdentifier: NSFileProviderItemIdentifier) {
274+
let uploadTaskRecord: UploadTaskRecord
275+
let itemMetadata: ItemMetadata
276+
let localCachedFileInfo: LocalCachedFileInfo
277+
do {
278+
itemMetadata = try getCachedMetadata(for: itemIdentifier)
279+
guard let retrievedLocalCachedFileInfo = try cachedFileManager.getLocalCachedFileInfo(for: itemMetadata) else {
280+
DDLogError("retryUpload - retrievedLocalCachedFileInfo is nil for identifier: \(itemIdentifier)")
281+
return
282+
}
283+
localCachedFileInfo = retrievedLocalCachedFileInfo
284+
uploadTaskRecord = try registerFileInUploadQueue(with: localCachedFileInfo.localURL, itemMetadata: itemMetadata)
285+
} catch {
286+
DDLogError("retryUpload - get existing uploadTaskRecord for identifier: \(itemIdentifier) failed with error: \(error)")
287+
return
288+
}
289+
let newestVersionLocallyCached = localCachedFileInfo.isCurrentVersion(lastModifiedDateInCloud: itemMetadata.lastModifiedDate)
290+
let localURL = localCachedFileInfo.localURL
291+
let item = FileProviderItem(metadata: itemMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached, localURL: localURL, error: nil)
292+
notificator?.signalUpdate(for: item)
293+
uploadFile(taskRecord: uploadTaskRecord).then { item in
294+
self.notificator?.signalUpdate(for: item)
295+
}
296+
}
297+
269298
func uploadFile(taskRecord: UploadTaskRecord, completionHandler: ((Error?) -> Void)? = nil) -> Promise<FileProviderItem> {
270299
let task: UploadTask
271300
do {
@@ -412,7 +441,7 @@ public class FileProviderAdapter: FileProviderAdapterType {
412441

413442
let localCachedFileInfo = try cachedFileManager.getLocalCachedFileInfo(for: itemMetadata)
414443
let newestVersionLocallyCached = localCachedFileInfo?.isCurrentVersion(lastModifiedDateInCloud: itemMetadata.lastModifiedDate) ?? false
415-
let item = FileProviderItem(metadata: itemMetadata, newestVersionLocallyCached: newestVersionLocallyCached)
444+
let item = FileProviderItem(metadata: itemMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: newestVersionLocallyCached)
416445
return MoveItemLocallyResult(item: item, reparentTaskRecord: taskRecord)
417446
}
418447

@@ -590,7 +619,7 @@ public class FileProviderAdapter: FileProviderAdapterType {
590619
} catch {
591620
return Promise(error)
592621
}
593-
if itemMetadata.statusCode == .isUploading {
622+
if itemMetadata.statusCode == .isUploading || itemMetadata.statusCode == .uploadError {
594623
return Promise(true)
595624
}
596625
return enumerateItems(for: identifier, withPageToken: nil).then { itemList -> Bool in
@@ -685,7 +714,7 @@ public class FileProviderAdapter: FileProviderAdapterType {
685714
try checkLocalItemCollision(for: cloudPath)
686715
let placeholderMetadata = ItemMetadata(name: name, type: .folder, size: nil, parentID: parentID, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: true)
687716
try itemMetadataManager.cacheMetadata(placeholderMetadata)
688-
return FileProviderItem(metadata: placeholderMetadata, newestVersionLocallyCached: true)
717+
return FileProviderItem(metadata: placeholderMetadata, domainIdentifier: domainIdentifier, newestVersionLocallyCached: true)
689718
}
690719

691720
/**
@@ -713,7 +742,7 @@ public class FileProviderAdapter: FileProviderAdapterType {
713742
do {
714743
_ = try deletionTaskManager.getTaskRecord(for: existingItemMetadata.id!)
715744
} catch DBManagerError.taskNotFound {
716-
throw NSError.fileProviderErrorForCollision(with: FileProviderItem(metadata: existingItemMetadata))
745+
throw NSError.fileProviderErrorForCollision(with: FileProviderItem(metadata: existingItemMetadata, domainIdentifier: domainIdentifier))
717746
}
718747
}
719748
}
@@ -774,22 +803,17 @@ public class FileProviderAdapter: FileProviderAdapterType {
774803
}
775804

776805
func convertFileProviderItemIdentifierToInt64(_ identifier: NSFileProviderItemIdentifier) throws -> Int64 {
777-
switch identifier {
778-
case .rootContainer:
779-
return itemMetadataManager.getRootContainerID()
780-
default:
781-
guard let id = Int64(identifier.rawValue) else {
782-
throw FileProviderAdapterError.unsupportedItemIdentifier
783-
}
784-
return id
806+
guard let id = identifier.databaseValue else {
807+
throw FileProviderAdapterError.unsupportedItemIdentifier
785808
}
809+
return id
786810
}
787811

788812
func convertIDToItemIdentifier(_ id: Int64) -> NSFileProviderItemIdentifier {
789813
if id == itemMetadataManager.getRootContainerID() {
790814
return .rootContainer
791815
}
792-
return NSFileProviderItemIdentifier("\(id)")
816+
return NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: id)
793817
}
794818

795819
struct LocalItemImportResult {

CryptomatorFileProvider/FileProviderAdapterManager.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding {
6464
adapter = cachedAdapter
6565
} else {
6666
DDLogDebug("Try to automatically unlock \(domain.displayName) - \(domain.identifier)")
67-
let autoUnlockItem = try autoUnlockVault(withVaultUID: vaultUID, dbPath: dbPath, delegate: delegate, notificator: notificator)
67+
let autoUnlockItem = try autoUnlockVault(withVaultUID: vaultUID, domainIdentifier: domain.identifier, dbPath: dbPath, delegate: delegate, notificator: notificator)
6868
adapterCache.cacheItem(autoUnlockItem, identifier: domain.identifier)
6969
adapter = autoUnlockItem.adapter
7070
}
@@ -78,7 +78,7 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding {
7878
return
7979
}
8080
let provider = try vaultManager.manualUnlockVault(withUID: domainIdentifier.rawValue, kek: kek)
81-
let item = try createAdapterCacheItem(cloudProvider: provider, dbPath: dbPath, delegate: delegate, notificator: notificator)
81+
let item = try createAdapterCacheItem(domainIdentifier: domainIdentifier, cloudProvider: provider, dbPath: dbPath, delegate: delegate, notificator: notificator)
8282
try vaultKeepUnlockedSettings.setLastUsedDate(Date(), forVaultUID: domainIdentifier.rawValue)
8383
adapterCache.cacheItem(item, identifier: domainIdentifier)
8484
let notificator = try notificatorManager.getFileProviderNotificator(for: NSFileProviderDomain(identifier: domainIdentifier, displayName: "", pathRelativeToDocumentStorage: ""))
@@ -119,7 +119,7 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding {
119119
try maintenanceManager.disableMaintenanceMode()
120120
}
121121

122-
private func autoUnlockVault(withVaultUID vaultUID: String, dbPath: URL, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType) throws -> AdapterCacheItem {
122+
private func autoUnlockVault(withVaultUID vaultUID: String, domainIdentifier: NSFileProviderDomainIdentifier, dbPath: URL, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType) throws -> AdapterCacheItem {
123123
guard vaultKeepUnlockedHelper.shouldAutoUnlockVault(withVaultUID: vaultUID) else {
124124
try masterkeyCacheManager.removeCachedMasterkey(forVaultUID: vaultUID)
125125
throw unlockMonitor.getUnlockError(forVaultUID: vaultUID)
@@ -128,12 +128,12 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding {
128128
throw unlockMonitor.getUnlockError(forVaultUID: vaultUID)
129129
}
130130
let provider = try vaultManager.createVaultProvider(withUID: vaultUID, masterkey: cachedMasterkey)
131-
let adapterCacheItem = try createAdapterCacheItem(cloudProvider: provider, dbPath: dbPath, delegate: delegate, notificator: notificator)
131+
let adapterCacheItem = try createAdapterCacheItem(domainIdentifier: domainIdentifier, cloudProvider: provider, dbPath: dbPath, delegate: delegate, notificator: notificator)
132132
notificator.refreshWorkingSet()
133133
return adapterCacheItem
134134
}
135135

136-
private func createAdapterCacheItem(cloudProvider: CloudProvider, dbPath: URL, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType) throws -> AdapterCacheItem {
136+
private func createAdapterCacheItem(domainIdentifier: NSFileProviderDomainIdentifier, cloudProvider: CloudProvider, dbPath: URL, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType) throws -> AdapterCacheItem {
137137
let database = try DatabaseHelper.getMigratedDB(at: dbPath)
138138
let itemMetadataManager = ItemMetadataDBManager(database: database)
139139
let cachedFileManager = CachedFileDBManager(database: database)
@@ -143,7 +143,8 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding {
143143
let itemEnumerationTaskManager = try ItemEnumerationTaskDBManager(database: database)
144144
let downloadTaskManager = try DownloadTaskDBManager(database: database)
145145
let maintenanceManager = MaintenanceDBManager(database: database)
146-
let adapter = FileProviderAdapter(uploadTaskManager: uploadTaskManager,
146+
let adapter = FileProviderAdapter(domainIdentifier: domainIdentifier,
147+
uploadTaskManager: uploadTaskManager,
147148
cachedFileManager: cachedFileManager,
148149
itemMetadataManager: itemMetadataManager,
149150
reparentTaskManager: reparentTaskManager,
@@ -154,7 +155,7 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding {
154155
provider: cloudProvider,
155156
notificator: notificator,
156157
localURLProvider: delegate)
157-
let workingSetObserver = WorkingSetObserver(database: database, notificator: notificator, uploadTaskManager: uploadTaskManager, cachedFileManager: cachedFileManager)
158+
let workingSetObserver = WorkingSetObserver(domainIdentifier: domainIdentifier, database: database, notificator: notificator, uploadTaskManager: uploadTaskManager, cachedFileManager: cachedFileManager)
158159
workingSetObserver.startObservation()
159160
return AdapterCacheItem(adapter: adapter, maintenanceManager: maintenanceManager, workingSetObserver: workingSetObserver)
160161
}

0 commit comments

Comments
 (0)