// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "OWSUploadingService.h" #import "Cryptography.h" #import "MIMETypeUtil.h" #import "NSNotificationCenter+OWS.h" #import "OWSError.h" #import "OWSMessageSender.h" #import "TSAttachmentStream.h" #import "TSNetworkManager.h" #import "TSOutgoingMessage.h" NS_ASSUME_NONNULL_BEGIN NSString *const kAttachmentUploadProgressNotification = @"kAttachmentUploadProgressNotification"; NSString *const kAttachmentUploadProgressKey = @"kAttachmentUploadProgressKey"; NSString *const kAttachmentUploadAttachmentIDKey = @"kAttachmentUploadAttachmentIDKey"; // Use a slightly non-zero value to ensure that the progress // indicator shows up as quickly as possible. static const CGFloat kAttachmentUploadProgressTheta = 0.001f; @interface OWSUploadingService () @property (nonatomic, readonly) TSNetworkManager *networkManager; @end @implementation OWSUploadingService - (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager { self = [super init]; if (!self) { return self; } _networkManager = networkManager; return self; } - (void)uploadAttachmentStream:(TSAttachmentStream *)attachmentStream message:(TSOutgoingMessage *)outgoingMessage success:(void (^)(void))successHandler failure:(RetryableFailureHandler)failureHandler { void (^successHandlerWrapper)(void) = ^{ [self fireProgressNotification:1 attachmentId:attachmentStream.uniqueId]; successHandler(); }; RetryableFailureHandler failureHandlerWrapper = ^(NSError *_Nonnull error) { [self fireProgressNotification:0 attachmentId:attachmentStream.uniqueId]; failureHandler(error); }; if (attachmentStream.serverId) { DDLogDebug(@"%@ Attachment previously uploaded.", self.logTag); successHandlerWrapper(); return; } [self fireProgressNotification:kAttachmentUploadProgressTheta attachmentId:attachmentStream.uniqueId]; TSRequest *allocateAttachment = [[TSAllocAttachmentRequest alloc] init]; [self.networkManager makeRequest:allocateAttachment success:^(NSURLSessionDataTask *task, id responseObject) { dispatch_async([OWSDispatch attachmentsQueue], ^{ // TODO can we move this queue specification up a level? if (![responseObject isKindOfClass:[NSDictionary class]]) { DDLogError(@"%@ unexpected response from server: %@", self.logTag, responseObject); NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); [error setIsRetryable:YES]; return failureHandlerWrapper(error); } NSDictionary *responseDict = (NSDictionary *)responseObject; UInt64 serverId = ((NSDecimalNumber *)[responseDict objectForKey:@"id"]).unsignedLongLongValue; NSString *location = [responseDict objectForKey:@"location"]; NSError *error; NSData *attachmentData = [attachmentStream readDataFromFileWithError:&error]; if (error) { DDLogError(@"%@ Failed to read attachment data with error:%@", self.logTag, error); [error setIsRetryable:YES]; return failureHandlerWrapper(error); } NSData *encryptionKey; NSData *digest; NSData *encryptedAttachmentData = [Cryptography encryptAttachmentData:attachmentData outKey:&encryptionKey outDigest:&digest]; attachmentStream.encryptionKey = encryptionKey; attachmentStream.digest = digest; [self uploadDataWithProgress:encryptedAttachmentData location:location attachmentId:attachmentStream.uniqueId success:^{ OWSAssert([NSThread isMainThread]); DDLogInfo(@"%@ Uploaded attachment: %p.", self.logTag, attachmentStream); attachmentStream.serverId = serverId; attachmentStream.isUploaded = YES; [attachmentStream saveAsyncWithCompletionBlock:successHandlerWrapper]; } failure:failureHandlerWrapper]; }); } failure:^(NSURLSessionDataTask *task, NSError *error) { DDLogError(@"%@ Failed to allocate attachment with error: %@", self.logTag, error); [error setIsRetryable:YES]; failureHandlerWrapper(error); }]; } - (void)uploadDataWithProgress:(NSData *)cipherText location:(NSString *)location attachmentId:(NSString *)attachmentId success:(void (^)(void))successHandler failure:(RetryableFailureHandler)failureHandler { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:location]]; request.HTTPMethod = @"PUT"; request.HTTPBody = cipherText; [request setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; NSURLSessionUploadTask *uploadTask; uploadTask = [manager uploadTaskWithRequest:request fromData:cipherText progress:^(NSProgress *_Nonnull uploadProgress) { [self fireProgressNotification:MAX(kAttachmentUploadProgressTheta, uploadProgress.fractionCompleted) attachmentId:attachmentId]; } completionHandler:^(NSURLResponse *_Nonnull response, id _Nullable responseObject, NSError *_Nullable error) { OWSAssert([NSThread isMainThread]); if (error) { [error setIsRetryable:YES]; return failureHandler(error); } NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; BOOL isValidResponse = (statusCode >= 200) && (statusCode < 400); if (!isValidResponse) { DDLogError(@"%@ Unexpected server response: %d", self.logTag, (int)statusCode); NSError *invalidResponseError = OWSErrorMakeUnableToProcessServerResponseError(); [invalidResponseError setIsRetryable:YES]; return failureHandler(invalidResponseError); } successHandler(); }]; [uploadTask resume]; } - (void)fireProgressNotification:(CGFloat)progress attachmentId:(NSString *)attachmentId { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter postNotificationNameAsync:kAttachmentUploadProgressNotification object:nil userInfo:@{ kAttachmentUploadProgressKey : @(progress), kAttachmentUploadAttachmentIDKey : attachmentId }]; } @end NS_ASSUME_NONNULL_END