Merge branch 'charlesmchen/attachmentRestore'

pull/1/head
Matthew Chen 7 years ago
commit c8cf5e01a7

@ -156,7 +156,7 @@
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496955B219B605E00DCFE74 /* PhotoLibrary.swift */; };
3496956021A2FC8100DCFE74 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3496955F21A2FC8100DCFE74 /* CloudKit.framework */; };
3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956221A301A100DCFE74 /* OWSBackupExportJob.m */; };
3496956F21A301A100DCFE74 /* OWSBackupLazyRestoreJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496956321A301A100DCFE74 /* OWSBackupLazyRestoreJob.swift */; };
3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496956321A301A100DCFE74 /* OWSBackupLazyRestore.swift */; };
3496957021A301A100DCFE74 /* OWSBackupIO.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956521A301A100DCFE74 /* OWSBackupIO.m */; };
3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956621A301A100DCFE74 /* OWSBackupImportJob.m */; };
3496957221A301A100DCFE74 /* OWSBackup.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956921A301A100DCFE74 /* OWSBackup.m */; };
@ -812,7 +812,7 @@
3496955B219B605E00DCFE74 /* PhotoLibrary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoLibrary.swift; sourceTree = "<group>"; };
3496955F21A2FC8100DCFE74 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
3496956221A301A100DCFE74 /* OWSBackupExportJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupExportJob.m; sourceTree = "<group>"; };
3496956321A301A100DCFE74 /* OWSBackupLazyRestoreJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupLazyRestoreJob.swift; sourceTree = "<group>"; };
3496956321A301A100DCFE74 /* OWSBackupLazyRestore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupLazyRestore.swift; sourceTree = "<group>"; };
3496956421A301A100DCFE74 /* OWSBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackup.h; sourceTree = "<group>"; };
3496956521A301A100DCFE74 /* OWSBackupIO.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupIO.m; sourceTree = "<group>"; };
3496956621A301A100DCFE74 /* OWSBackupImportJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupImportJob.m; sourceTree = "<group>"; };
@ -1778,7 +1778,7 @@
3496956521A301A100DCFE74 /* OWSBackupIO.m */,
3496956721A301A100DCFE74 /* OWSBackupJob.h */,
3496956A21A301A100DCFE74 /* OWSBackupJob.m */,
3496956321A301A100DCFE74 /* OWSBackupLazyRestoreJob.swift */,
3496956321A301A100DCFE74 /* OWSBackupLazyRestore.swift */,
);
path = Backup;
sourceTree = "<group>";
@ -3568,7 +3568,7 @@
3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */,
B90418E6183E9DD40038554A /* DateUtil.m in Sources */,
340FC8BD204DAC8D007AEB0F /* ShowGroupMembersViewController.m in Sources */,
3496956F21A301A100DCFE74 /* OWSBackupLazyRestoreJob.swift in Sources */,
3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */,
459311FC1D75C948008DD4F0 /* OWSDeviceTableViewCell.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

@ -1212,11 +1212,6 @@ static NSTimeInterval launchStartedAt;
[self.messageManager startObserving];
#ifdef DEBUG
// Resume lazy restore.
[OWSBackupLazyRestoreJob runAsync];
#endif
[self.udManager setup];
}

@ -88,7 +88,6 @@ public class ConversationMediaView: UIView {
}
guard attachmentPointer.pointerType == .incoming else {
// TODO: Show "restoring" indicator and possibly progress.
owsFailDebug("Attachment is restoring from backup.")
configure(forError: .missing)
return
}

@ -100,7 +100,7 @@ NS_ASSUME_NONNULL_BEGIN
[OWSBackup.sharedManager
checkCanImportBackup:^(BOOL value) {
OWSLogInfo(@"has backup available for import? %d", value);
OWSLogInfo(@"has backup available for import? %d", value);
}
failure:^(NSError *error){
// Do nothing.

@ -123,7 +123,7 @@ public class BackupRestoreViewController: OWSTableViewController {
backup.setHasPendingRestoreDecision(false)
dismiss(animated: true)
showHomeView()
}
@objc

@ -58,6 +58,9 @@ import SignalMessaging
@objc
public var backup: OWSBackup
@objc
public var backupLazyRestore: BackupLazyRestore
private override init() {
self.callMessageHandler = WebRTCCallMessageHandler()
self.callService = CallService()
@ -70,6 +73,7 @@ import SignalMessaging
self.pushManager = PushManager()
self.sessionResetJobQueue = SessionResetJobQueue()
self.backup = OWSBackup()
self.backupLazyRestore = BackupLazyRestore()
super.init()

@ -95,9 +95,7 @@ NSError *OWSBackupErrorWithDescription(NSString *description);
- (NSArray<NSString *> *)attachmentIdsForLazyRestore;
- (void)lazyRestoreAttachment:(TSAttachmentPointer *)attachment
backupIO:(OWSBackupIO *)backupIO
completion:(OWSBackupBoolBlock)completion;
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachment backupIO:(OWSBackupIO *)backupIO;
@end

@ -63,14 +63,17 @@ import PromiseKit
// complete.
@objc
public class func saveEphemeralFileToCloudObjc(recipientId: String,
label: String,
fileUrl: URL) -> AnyPromise {
return AnyPromise(saveEphemeralFileToCloud(recipientId: recipientId,
label: label,
fileUrl: fileUrl))
}
public class func saveEphemeralFileToCloud(recipientId: String,
label: String,
fileUrl: URL) -> Promise<String> {
let recordName = "\(recordNamePrefix(forRecipientId: recipientId))ephemeralFile-\(NSUUID().uuidString)"
let recordName = "\(recordNamePrefix(forRecipientId: recipientId))ephemeral-\(label)-\(NSUUID().uuidString)"
return saveFileToCloud(fileUrl: fileUrl,
recordName: recordName,
recordType: signalBackupRecordType)
@ -209,6 +212,8 @@ import PromiseKit
private class func saveRecordToCloud(record: CKRecord,
remainingRetries: Int) -> Promise<String> {
Logger.verbose("saveRecordToCloud \(record.recordID.recordName)")
return Promise { resolver in
let saveOperation = CKModifyRecordsOperation(recordsToSave: [record ], recordIDsToDelete: nil)
saveOperation.modifyRecordsCompletionBlock = { (records, recordIds, error) in
@ -389,6 +394,8 @@ import PromiseKit
private class func checkForFileInCloud(recordName: String,
remainingRetries: Int) -> Promise<CKRecord?> {
Logger.verbose("checkForFileInCloud \(recordName)")
let (promise, resolver) = Promise<CKRecord?>.pending()
let recordId = CKRecordID(recordName: recordName)
@ -643,6 +650,8 @@ import PromiseKit
private class func downloadFromCloud(recordName: String,
remainingRetries: Int) -> Promise<CKAsset> {
Logger.verbose("downloadFromCloud \(recordName)")
let (promise, resolver) = Promise<CKAsset>.pending()
let recordId = CKRecordID(recordName: recordName)

@ -717,6 +717,7 @@ NS_ASSUME_NONNULL_BEGIN
// Add one for the manifest
NSUInteger unsavedCount = (self.unsavedDatabaseItems.count + self.unsavedAttachmentExports.count + 1);
NSUInteger savedCount = (self.savedDatabaseItems.count + self.savedAttachmentItems.count);
// Ignore localProfileAvatarItem for now.
CGFloat progress = (savedCount / (CGFloat)(unsavedCount + savedCount));
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_EXPORT_PHASE_UPLOAD",
@ -756,8 +757,9 @@ NS_ASSUME_NONNULL_BEGIN
return [OWSBackupAPI
saveEphemeralFileToCloudObjcWithRecipientId:self.recipientId
fileUrl:[NSURL fileURLWithPath:item.encryptedItem
.filePath]];
label:@"database"
fileUrl:[NSURL
fileURLWithPath:item.encryptedItem.filePath]];
})
.thenInBackground(^(NSString *recordName) {
item.recordName = recordName;
@ -907,6 +909,7 @@ NS_ASSUME_NONNULL_BEGIN
exportItem.encryptedItem = encryptedItem;
return [OWSBackupAPI saveEphemeralFileToCloudObjcWithRecipientId:self.recipientId
label:@"local-profile-avatar"
fileUrl:[NSURL fileURLWithPath:encryptedItem.filePath]]
.thenInBackground(^(NSString *recordName) {
exportItem.recordName = recordName;
@ -1029,6 +1032,12 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(![activeRecordNames containsObject:item.recordName]);
[activeRecordNames addObject:item.recordName];
}
if (self.localProfileAvatarItem) {
OWSBackupExportItem *item = self.localProfileAvatarItem;
OWSAssertDebug(item.recordName.length > 0);
OWSAssertDebug(![activeRecordNames containsObject:item.recordName]);
[activeRecordNames addObject:item.recordName];
}
OWSAssertDebug(self.manifestItem.recordName.length > 0);
OWSAssertDebug(![activeRecordNames containsObject:self.manifestItem.recordName]);
[activeRecordNames addObject:self.manifestItem.recordName];

@ -63,6 +63,11 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
return AppEnvironment.shared.backup;
}
- (OWSBackupLazyRestore *)backupLazyRestore
{
return AppEnvironment.shared.backupLazyRestore;
}
#pragma mark -
- (NSArray<OWSBackupFragment *> *)databaseItems
@ -129,14 +134,21 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
OWSAssertDebug(self.databaseItems);
OWSAssertDebug(self.attachmentsItems);
// These items should be downloaded immediately.
NSMutableArray<OWSBackupFragment *> *allItems = [NSMutableArray new];
[allItems addObjectsFromArray:self.databaseItems];
// TODO: We probably want to remove this.
[allItems addObjectsFromArray:self.attachmentsItems];
if (self.manifest.localProfileAvatarItem) {
[allItems addObject:self.manifest.localProfileAvatarItem];
}
// Make a copy of the blockingItems before we add
// the attachment items.
NSArray<OWSBackupFragment *> *blockingItems = [allItems copy];
// Attachment items can be downloaded later;
// they will can be lazy-restored.
[allItems addObjectsFromArray:self.attachmentsItems];
// Record metadata for all items, so that we can re-use them in incremental backups after the restore.
[self.primaryStorage.newDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (OWSBackupFragment *item in allItems) {
@ -144,7 +156,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
}
}];
return [self downloadFilesFromCloud:allItems]
return [self downloadFilesFromCloud:blockingItems]
.thenInBackground(^{
return [self restoreDatabase];
})
@ -157,17 +169,18 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
.thenInBackground(^{
return [self restoreAttachmentFiles];
})
.thenInBackground(^{
// Kick off lazy restore.
[OWSBackupLazyRestoreJob runAsync];
.then(^{
// Kick off lazy restore on main thread.
[self.backupLazyRestore runIfNecessary];
[self.profileManager fetchLocalUsersProfile];
[self.tsAccountManager updateAccountAttributes];
// Make sure backup is enabled once we complete
// a backup restore.
[OWSBackup.sharedManager setIsBackupEnabled:YES];
})
.thenInBackground(^{
[self.tsAccountManager updateAccountAttributes];
[self succeed];
});
@ -187,7 +200,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
return [AnyPromise promiseWithValue:@(1)];
}
- (AnyPromise *)downloadFilesFromCloud:(NSMutableArray<OWSBackupFragment *> *)items
- (AnyPromise *)downloadFilesFromCloud:(NSArray<OWSBackupFragment *> *)items
{
OWSAssertDebug(items.count > 0);
@ -244,7 +257,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
- (AnyPromise *)restoreLocalProfile
{
OWSLogVerbose(@": %zd", self.attachmentsItems.count);
OWSLogVerbose(@"");
if (self.isComplete) {
// Job was aborted.
@ -257,12 +270,7 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
if (self.manifest.localProfileAvatarItem) {
OWSBackupFragment *item = self.manifest.localProfileAvatarItem;
if (item.recordName.length < 1) {
OWSLogError(@"local profile avatar was not downloaded.");
// Ignore errors related to local profile.
return [AnyPromise promiseWithValue:@(1)];
}
if (!item.uncompressedDataLength || item.uncompressedDataLength.unsignedIntValue < 1) {
OWSLogError(@"database snapshot missing size.");
OWSLogError(@"item was not downloaded.");
// Ignore errors related to local profile.
return [AnyPromise promiseWithValue:@(1)];
}
@ -286,6 +294,8 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe
}
}
OWSLogVerbose(@"local profile name: %@, avatar: %d", localProfileName, localProfileAvatar != nil);
if (localProfileName.length > 0 || localProfileAvatar) {
AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[self.profileManager updateLocalProfileName:localProfileName

@ -224,7 +224,7 @@ NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
contents.localProfileAvatarItem = localProfileAvatarItems.firstObject;
if ([localProfileName isKindOfClass:[NSString class]]) {
contents.localProfileName = localProfileName;
} else {
} else if (localProfileName) {
OWSFailDebug(@"Invalid localProfileName: %@", [localProfileName class]);
}

@ -0,0 +1,141 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
import SignalServiceKit
@objc(OWSBackupLazyRestore)
public class BackupLazyRestore: NSObject {
// MARK: - Dependencies
private var backup: OWSBackup {
return AppEnvironment.shared.backup
}
private var primaryStorage: OWSPrimaryStorage {
return SSKEnvironment.shared.primaryStorage
}
private var tsAccountManager: TSAccountManager {
return TSAccountManager.sharedInstance()
}
// MARK: -
private var isRunning = false
private var isComplete = false
@objc
public required override init() {
super.init()
SwiftSingletons.register(self)
AppReadiness.runNowOrWhenAppDidBecomeReady {
self.runIfNecessary()
}
NotificationCenter.default.addObserver(forName: .OWSApplicationDidBecomeActive, object: nil, queue: nil) { _ in
self.runIfNecessary()
}
NotificationCenter.default.addObserver(forName: .RegistrationStateDidChange, object: nil, queue: nil) { _ in
self.runIfNecessary()
}
NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: nil) { _ in
self.runIfNecessary()
}
}
// MARK: -
private let backgroundQueue = DispatchQueue.global(qos: .background)
@objc
public func runIfNecessary() {
AssertIsOnMainThread()
guard CurrentAppContext().isMainAppAndActive else {
return
}
guard tsAccountManager.isRegisteredAndReady() else {
return
}
guard !isRunning, !isComplete else {
return
}
isRunning = true
backgroundQueue.async {
self.restoreAttachments()
}
}
private func restoreAttachments() {
let temporaryDirectory = OWSTemporaryDirectory()
let jobTempDirPath = (temporaryDirectory as NSString).appendingPathComponent(NSUUID().uuidString)
guard OWSFileSystem.ensureDirectoryExists(jobTempDirPath) else {
Logger.error("could not create temp directory.")
complete(errorCount: 1)
return
}
let backupIO = OWSBackupIO(jobTempDirPath: jobTempDirPath)
let attachmentIds = backup.attachmentIdsForLazyRestore()
guard attachmentIds.count > 0 else {
Logger.info("No attachments need lazy restore.")
complete(errorCount: 0)
return
}
Logger.info("Lazy restoring \(attachmentIds.count) attachments.")
tryToRestoreNextAttachment(attachmentIds: attachmentIds, errorCount: 0, backupIO: backupIO)
}
private func tryToRestoreNextAttachment(attachmentIds: [String], errorCount: UInt, backupIO: OWSBackupIO) {
var attachmentIdsCopy = attachmentIds
guard let attachmentId = attachmentIdsCopy.popLast() else {
// This job is done.
Logger.verbose("job is done.")
complete(errorCount: errorCount)
return
}
guard let attachmentPointer = TSAttachment.fetch(uniqueId: attachmentId) as? TSAttachmentPointer else {
Logger.warn("could not load attachment.")
// Not necessarily an error.
// The attachment might have been deleted since the job began.
// Continue trying to restore the other attachments.
tryToRestoreNextAttachment(attachmentIds: attachmentIds, errorCount: errorCount + 1, backupIO: backupIO)
return
}
backup.lazyRestoreAttachment(attachmentPointer,
backupIO: backupIO)
.done(on: self.backgroundQueue) { _ in
Logger.info("Restored attachment.")
// Continue trying to restore the other attachments.
self.tryToRestoreNextAttachment(attachmentIds: attachmentIdsCopy, errorCount: errorCount, backupIO: backupIO)
}.catch(on: self.backgroundQueue) { (error) in
Logger.error("Could not restore attachment: \(error)")
// Continue trying to restore the other attachments.
self.tryToRestoreNextAttachment(attachmentIds: attachmentIdsCopy, errorCount: errorCount + 1, backupIO: backupIO)
}.retainUntilComplete()
}
private func complete(errorCount: UInt) {
Logger.verbose("")
DispatchQueue.main.async {
self.isRunning = false
if errorCount == 0 {
self.isComplete = true
}
}
}
}

@ -1,92 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
import SignalServiceKit
@objc
public class OWSBackupLazyRestoreJob: NSObject {
let primaryStorage: OWSPrimaryStorage
private var jobTempDirPath: String?
deinit {
if let jobTempDirPath = self.jobTempDirPath {
DispatchQueue.global().async {
OWSFileSystem.deleteFile(jobTempDirPath)
}
}
}
@objc
public class func runAsync() {
OWSBackupLazyRestoreJob().runAsync()
}
public override init() {
self.primaryStorage = OWSPrimaryStorage.shared()
}
private func runAsync() {
AssertIsOnMainThread()
DispatchQueue.global().async {
self.restoreAttachments()
}
}
private func restoreAttachments() {
let temporaryDirectory = OWSTemporaryDirectory()
let jobTempDirPath = (temporaryDirectory as NSString).appendingPathComponent(NSUUID().uuidString)
guard OWSFileSystem.ensureDirectoryExists(jobTempDirPath) else {
Logger.error("could not create temp directory.")
return
}
self.jobTempDirPath = jobTempDirPath
let backupIO = OWSBackupIO(jobTempDirPath: jobTempDirPath)
let attachmentIds = OWSBackup.shared().attachmentIdsForLazyRestore()
guard attachmentIds.count > 0 else {
Logger.info("No attachments need lazy restore.")
return
}
Logger.info("Lazy restoring \(attachmentIds.count) attachments.")
self.tryToRestoreNextAttachment(attachmentIds: attachmentIds, backupIO: backupIO)
}
private func tryToRestoreNextAttachment(attachmentIds: [String], backupIO: OWSBackupIO) {
var attachmentIdsCopy = attachmentIds
guard let attachmentId = attachmentIdsCopy.last else {
// This job is done.
Logger.verbose("job is done.")
return
}
attachmentIdsCopy.removeLast()
guard let attachmentPointer = TSAttachment.fetch(uniqueId: attachmentId) as? TSAttachmentPointer else {
Logger.warn("could not load attachment.")
// Not necessarily an error.
// The attachment might have been deleted since the job began.
// Continue trying to restore the other attachments.
tryToRestoreNextAttachment(attachmentIds: attachmentIds, backupIO: backupIO)
return
}
OWSBackup.shared().lazyRestoreAttachment(attachmentPointer,
backupIO: backupIO,
completion: { (success) in
if success {
Logger.info("restored attachment.")
} else {
Logger.warn("could not restore attachment.")
}
// Continue trying to restore the other attachments.
self.tryToRestoreNextAttachment(attachmentIds: attachmentIdsCopy, backupIO: backupIO)
})
}
}

@ -1447,7 +1447,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
- (void)updateProfileAvatarCache:(nullable UIImage *)image filename:(NSString *)filename
{
OWSAssertDebug(filename.length > 0);
OWSAssertDebug(image);
@synchronized(self.profileAvatarImageCache)
{

Loading…
Cancel
Save