From 48ba6afc4fa2cab9ac2b50240e0e6b92a95e8c3b Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 28 Apr 2020 16:26:07 +0200 Subject: [PATCH 1/3] - add initial parsing support for Tus HTTP Headers and compact storage in OCItem as UInt64 bitfield --- ownCloudSDK.xcodeproj/project.pbxproj | 16 ++++ ownCloudSDK/Connection/OCConnection.m | 22 +++++- ownCloudSDK/HTTP/Response/OCHTTPResponse.h | 2 +- ownCloudSDK/HTTP/TUS/OCTUSHeader.h | 64 ++++++++++++++++ ownCloudSDK/HTTP/TUS/OCTUSHeader.m | 88 ++++++++++++++++++++++ ownCloudSDK/Item/OCItem.h | 15 ++++ ownCloudSDK/Item/OCItem.m | 47 +++++++++++- ownCloudSDK/ownCloudSDK.h | 2 + 8 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 ownCloudSDK/HTTP/TUS/OCTUSHeader.h create mode 100644 ownCloudSDK/HTTP/TUS/OCTUSHeader.m diff --git a/ownCloudSDK.xcodeproj/project.pbxproj b/ownCloudSDK.xcodeproj/project.pbxproj index 7aecf0e2..0281a539 100644 --- a/ownCloudSDK.xcodeproj/project.pbxproj +++ b/ownCloudSDK.xcodeproj/project.pbxproj @@ -517,6 +517,8 @@ DCEEB2F22047094500189B9A /* NSData+OCHash.m in Sources */ = {isa = PBXBuildFile; fileRef = DCEEB2F02047094500189B9A /* NSData+OCHash.m */; }; DCEEB2F5204802CF00189B9A /* OCIssue.h in Headers */ = {isa = PBXBuildFile; fileRef = DCEEB2F3204802CF00189B9A /* OCIssue.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCEEB2F6204802CF00189B9A /* OCIssue.m in Sources */ = {isa = PBXBuildFile; fileRef = DCEEB2F4204802CF00189B9A /* OCIssue.m */; }; + DCF39B552458268E00DEA137 /* OCTUSHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = DCF39B532458268E00DEA137 /* OCTUSHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCF39B562458268E00DEA137 /* OCTUSHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = DCF39B542458268E00DEA137 /* OCTUSHeader.m */; }; DCFBACF121BAA77F00943F76 /* largePropFindResponse1000.xml in Resources */ = {isa = PBXBuildFile; fileRef = DCFBACF021BAA77F00943F76 /* largePropFindResponse1000.xml */; }; DCFBACF721BAB35A00943F76 /* PerformanceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFBACF621BAB35A00943F76 /* PerformanceTests.m */; }; DCFBACF921BAB5F500943F76 /* OCDetailedPerformanceTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFBACF821BAB5F500943F76 /* OCDetailedPerformanceTestCase.m */; }; @@ -1195,6 +1197,8 @@ DCEEB2F02047094500189B9A /* NSData+OCHash.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+OCHash.m"; sourceTree = ""; }; DCEEB2F3204802CF00189B9A /* OCIssue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCIssue.h; sourceTree = ""; }; DCEEB2F4204802CF00189B9A /* OCIssue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCIssue.m; sourceTree = ""; }; + DCF39B532458268E00DEA137 /* OCTUSHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCTUSHeader.h; sourceTree = ""; }; + DCF39B542458268E00DEA137 /* OCTUSHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCTUSHeader.m; sourceTree = ""; }; DCFBACF021BAA77F00943F76 /* largePropFindResponse1000.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = largePropFindResponse1000.xml; sourceTree = ""; }; DCFBACF621BAB35A00943F76 /* PerformanceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PerformanceTests.m; sourceTree = ""; }; DCFBACF821BAB5F500943F76 /* OCDetailedPerformanceTestCase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCDetailedPerformanceTestCase.m; sourceTree = ""; }; @@ -1802,6 +1806,7 @@ DC27BBBC230498A4002CC2F8 /* Cookies */, DC7014182209C57B009D4FD9 /* Request */, DC701481220B0865009D4FD9 /* Response */, + DCF39B572458290200DEA137 /* TUS */, ); path = HTTP; sourceTree = ""; @@ -2699,6 +2704,15 @@ path = Toolkit; sourceTree = ""; }; + DCF39B572458290200DEA137 /* TUS */ = { + isa = PBXGroup; + children = ( + DCF39B542458268E00DEA137 /* OCTUSHeader.m */, + DCF39B532458268E00DEA137 /* OCTUSHeader.h */, + ); + path = TUS; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -2772,6 +2786,7 @@ DC0AE4F22311C75300428681 /* OCKeyValueStack.h in Headers */, DC1D4D3720DBD58E005A3DFC /* OCFile.h in Headers */, DC34227A217CAA0B00705508 /* OCIPNotificationCenter.h in Headers */, + DCF39B552458268E00DEA137 /* OCTUSHeader.h in Headers */, DC8EB2FE23951AAB009148F9 /* OCAuthenticationBrowserSession.h in Headers */, DCEEB2F5204802CF00189B9A /* OCIssue.h in Headers */, DC35969A2240EC0A00C4D6E6 /* OCQueryCondition+Item.h in Headers */, @@ -3316,6 +3331,7 @@ DC359693223FA7CC00C4D6E6 /* OCQueryCondition.m in Sources */, DCFFF57F20D3A51C0096D2D3 /* OCSyncContext.m in Sources */, DCA36D4E22A6B14200265534 /* OCPKCE.m in Sources */, + DCF39B562458268E00DEA137 /* OCTUSHeader.m in Sources */, DCAEB06A21FA617D0067E147 /* OCActivity.m in Sources */, DC2FED61228D5589004FDEC6 /* OCCore+Favorites.m in Sources */, DC29F4C324323C4900347658 /* OCSyncIssueTemplate.m in Sources */, diff --git a/ownCloudSDK/Connection/OCConnection.m b/ownCloudSDK/Connection/OCConnection.m index 4392b172..f3387bee 100644 --- a/ownCloudSDK/Connection/OCConnection.m +++ b/ownCloudSDK/Connection/OCConnection.m @@ -1272,11 +1272,31 @@ - (void)_handleRetrieveItemListAtPathResult:(OCHTTPRequest *)request error:(NSEr } break; - default: + default: { event.path = request.userInfo[@"path"]; event.depth = [(NSNumber *)request.userInfo[@"depth"] unsignedIntegerValue]; + OCTUSHeader *tusHeader; + + if ((tusHeader = [[OCTUSHeader alloc] initWithHTTPHeaderFields:request.httpResponse.headerFields]) != nil) + { + OCTUSSupport tusSupportLevel = tusHeader.supportFlags; + + if (tusSupportLevel != OCTUSSupportNone) + { + for (OCItem *item in items) + { + if ([item.path isEqual:event.path]) + { + item.tusInfo = tusHeader.info; + break; + } + } + } + } + event.result = items; + } break; } diff --git a/ownCloudSDK/HTTP/Response/OCHTTPResponse.h b/ownCloudSDK/HTTP/Response/OCHTTPResponse.h index 65081372..18ceb36b 100644 --- a/ownCloudSDK/HTTP/Response/OCHTTPResponse.h +++ b/ownCloudSDK/HTTP/Response/OCHTTPResponse.h @@ -39,7 +39,7 @@ NS_ASSUME_NONNULL_BEGIN @property(strong,nullable) NSError *certificateValidationError; //!< Any error that occured during validation of the certificate (if any). @property(strong,nullable) OCHTTPStatus *status; //!< The HTTP status returned by the server -@property(strong,nullable) NSDictionary *headerFields; //!< All HTTP header fields +@property(strong,nullable) OCHTTPHeaderFields headerFields; //!< All HTTP header fields @property(strong,nullable,nonatomic) NSHTTPURLResponse *httpURLResponse; //!< The NSHTTPURLResponse returned by the server. If set, is used to populate httpStatus and allHTTPHeaderFields. diff --git a/ownCloudSDK/HTTP/TUS/OCTUSHeader.h b/ownCloudSDK/HTTP/TUS/OCTUSHeader.h new file mode 100644 index 00000000..4f882ac6 --- /dev/null +++ b/ownCloudSDK/HTTP/TUS/OCTUSHeader.h @@ -0,0 +1,64 @@ +// +// OCTUSHeader.h +// ownCloudSDK +// +// Created by Felix Schwarz on 28.04.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +#import +#import "OCHTTPTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NSString* OCTUSVersion; +typedef NSString* OCTUSExtension; + +typedef NSString* OCTUSHeaderJSON; + +typedef NS_OPTIONS(UInt8, OCTUSSupport) +{ + OCTUSSupportNone, + OCTUSSupportAvailable = (1<<0), + OCTUSSupportExtensionCreation = (1<<1), + OCTUSSupportExtensionCreationWithUpload = (1<<2), + OCTUSSupportExtensionExpiration = (1<<3) +}; + +typedef struct { + UInt8 reserved : 8; + UInt64 maximumSize : 48; + OCTUSSupport tusSupport : 8; +} OCTUSInfoPrivate; + +typedef UInt64 OCTUSInfo; // encodes OCTUSSupport, maxSize and more + +#define OCTUSInfoGetSupport(info) ((OCTUSInfoPrivate *)&info)->tusSupport +#define OCTUSInfoSetSupport(info,flags) ((OCTUSInfoPrivate *)&info)->tusSupport = (flags) + +#define OCTUSInfoGetMaximumSize(info) ((OCTUSInfoPrivate *)&info)->maximumSize +#define OCTUSInfoSetMaximumSize(info,maxSize) ((OCTUSInfoPrivate *)&info)->maximumSize = (maxSize) + +@interface OCTUSHeader : NSObject // + +@property(strong,nullable) OCTUSVersion version; //!< Corresponds to "Tus-Resumable" +@property(strong,nullable) NSArray *versions; //!< Corresponds to "Tus-Version" header (where available), with fallback to "Tus-Resumable" +@property(strong,nullable) NSArray *extensions; //!< Corresponds to "Tus-Extension" header + +@property(strong,nullable) NSNumber *maximumSize; //!< Corresponds to "Tus-Max-Size" header + +@property(strong,nullable) NSNumber *uploadOffset; //!< Corresponds to "Upload-Offset" header +@property(strong,nullable) NSNumber *uploadLength; //!< Corresponds to "Upload-Length" header + +@property(readonly,nonatomic) OCTUSSupport supportFlags; //!< Returns TUS support info compressed as set of flags +@property(readonly,nonatomic) OCTUSInfo info; //!< Returns TUS info compressed to an integer + +- (instancetype)initWithHTTPHeaderFields:(OCHTTPHeaderFields)headerFields; +//- (OCHTTPHeaderFields)httpHeaderFields; + +//- (instancetype)initWithHeaderJSON:(OCTUSHeaderJSON)headerJSON; +//- (OCTUSHeaderJSON)headerJSON; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/HTTP/TUS/OCTUSHeader.m b/ownCloudSDK/HTTP/TUS/OCTUSHeader.m new file mode 100644 index 00000000..8ed27f2b --- /dev/null +++ b/ownCloudSDK/HTTP/TUS/OCTUSHeader.m @@ -0,0 +1,88 @@ +// +// OCTUSHeader.m +// ownCloudSDK +// +// Created by Felix Schwarz on 28.04.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +#import "OCTUSHeader.h" + +@implementation OCTUSHeader + +- (instancetype)initWithHTTPHeaderFields:(OCHTTPHeaderFields)headerFields +{ + if ((self = [super init]) != nil) + { + [self _populateFromHTTPHeaders:headerFields]; + } + + return (self); +} + +- (void)_populateFromHTTPHeaders:(OCHTTPHeaderFields)headerFields +{ + NSString *tusVersion, *tusResumable, *tusExtensions, *tusMaxSize, *uploadOffset, *uploadLength; + + if ((tusVersion = headerFields[@"Tus-Version"]) != nil) + { + _versions = [tusVersion componentsSeparatedByString:@","]; + } + + if ((tusResumable = headerFields[@"Tus-Resumable"]) != nil) + { + _version = tusResumable; + if (_versions == nil) + { + _versions = @[ _version ]; + } + } + + if ((tusExtensions = headerFields[@"Tus-Extension"]) != nil) + { + _extensions = [tusExtensions componentsSeparatedByString:@","]; + } + + if ((tusMaxSize = headerFields[@"Tus-Max-Size"]) != nil) + { + _maximumSize = @(tusMaxSize.longLongValue); + } + + if ((uploadOffset = headerFields[@"Upload-Offset"]) != nil) + { + _uploadOffset = @(uploadOffset.longLongValue); + } + + if ((uploadLength = headerFields[@"Upload-Length"]) != nil) + { + _uploadLength = @(uploadLength.longLongValue); + } +} + +- (OCTUSSupport)supportFlags +{ + OCTUSSupport support = OCTUSSupportNone; + + if (_versions.count > 0) + { + support = OCTUSSupportAvailable; + + if ([_extensions containsObject:@"creation"]) { support |= OCTUSSupportExtensionCreation; } + if ([_extensions containsObject:@"creation-with-upload"]) { support |= OCTUSSupportExtensionCreationWithUpload; } + if ([_extensions containsObject:@"expiration"]) { support |= OCTUSSupportExtensionExpiration; } + } + + return (support); +} + +- (OCTUSInfo)info +{ + OCTUSInfo info = 0; + + OCTUSInfoSetSupport(info, self.supportFlags); + OCTUSInfoSetMaximumSize(info, self.maximumSize.unsignedLongLongValue); + + return (info); +} + +@end diff --git a/ownCloudSDK/Item/OCItem.h b/ownCloudSDK/Item/OCItem.h index edcb4122..5472f461 100644 --- a/ownCloudSDK/Item/OCItem.h +++ b/ownCloudSDK/Item/OCItem.h @@ -20,6 +20,7 @@ #import "OCItemThumbnail.h" #import "OCItemVersionIdentifier.h" #import "OCClaim.h" +#import "OCTUSHeader.h" @class OCFile; @class OCCore; @@ -73,6 +74,15 @@ typedef NS_ENUM(NSInteger, OCItemCloudStatus) OCItemCloudStatusLocalOnly //!< Item only exists locally. There's no remote copy. }; +typedef NS_OPTIONS(NSInteger, OCUploadMethod) +{ + OCUploadMethodUnknown = 0, + + OCUploadMethodStandard = (1 << 0), //!< Standard HTTP PUT uploads supported + OCUploadMethodChunked = (1 << 1), //!< OC10 chunked uploads supported + OCUploadMethodTUS = (1 << 2) //!< TUS uploads supported +}; + #import "OCShare.h" NS_ASSUME_NONNULL_BEGIN @@ -149,6 +159,11 @@ NS_ASSUME_NONNULL_BEGIN @property(strong,nullable) NSURL *privateLink; //!< Private link for the item. This property is used as a cache. Please use -[OCCore retrievePrivateLinkForItem:..] to request the private link for an item. +@property(assign,nonatomic) OCTUSInfo tusInfo; //!< For folders only: compressed Tus info; undefined for files +@property(readonly,nonatomic) OCTUSSupport tusSupport; //!< For folders only: Tus support level; undefined for files +@property(readonly,nonatomic) UInt64 tusMaximumSize; //!< For folders only: Tus maximum chunk size; undefined for files +// @property(strong,nullable) OCTUSHeader *tusHeader; //!< For folders only: detailed Tus support info (optional); nil for files + @property(readonly,nonatomic) OCItemThumbnailAvailability thumbnailAvailability; //!< Availability of thumbnails for this item. If OCItemThumbnailAvailabilityUnknown, call -[OCCore retrieveThumbnailFor:resultHandler:] to update it. @property(nullable,strong,nonatomic) OCItemThumbnail *thumbnail; //!< Thumbnail for the item. diff --git a/ownCloudSDK/Item/OCItem.m b/ownCloudSDK/Item/OCItem.m index 31035468..58480e71 100644 --- a/ownCloudSDK/Item/OCItem.m +++ b/ownCloudSDK/Item/OCItem.m @@ -118,6 +118,8 @@ - (void)encodeWithCoder:(NSCoder *)coder [coder encodeObject:_privateLink forKey:@"privateLink"]; + [coder encodeInt64:(int64_t)_tusInfo forKey:@"tusInfo"]; + [coder encodeObject:_databaseID forKey:@"databaseID"]; [coder encodeObject:_quotaBytesRemaining forKey:@"quotaBytesRemaining"]; @@ -177,6 +179,8 @@ - (instancetype)initWithCoder:(NSCoder *)decoder _privateLink = [decoder decodeObjectOfClass:[NSURL class] forKey:@"privateLink"]; + _tusInfo = (UInt64)[decoder decodeInt64ForKey:@"tusInfo"]; + _databaseID = [decoder decodeObjectOfClass:[NSValue class] forKey:@"databaseID"]; _quotaBytesRemaining = [decoder decodeObjectOfClass:[NSNumber class] forKey:@"quotaBytesRemaining"]; @@ -584,6 +588,8 @@ - (void)copyMetadataFrom:(OCItem *)item except:(NSSet *)exc CloneMetadata(@"privateLink"); + CloneMetadata(@"tusInfo"); + CloneMetadata(@"checksums"); CloneMetadata(@"databaseID"); @@ -668,6 +674,45 @@ - (NSString *)_shareTypesDescription return (shareTypesDescription); } +- (OCTUSSupport)tusSupport +{ + return (OCTUSInfoGetSupport(_tusInfo)); +} + +- (UInt64)tusMaximumSize +{ + return (OCTUSInfoGetMaximumSize(_tusInfo)); +} + +- (NSString *)_tusSupportDescription +{ + NSString *tusSupportDescription = nil; + OCTUSSupport support = OCTUSInfoGetSupport(_tusInfo); + + if (support != OCTUSSupportNone) + { + tusSupportDescription = @", tusSupport:"; + #define AppendTusExtension(extensionFlag, name) \ + if ((support & extensionFlag) != 0) \ + { \ + tusSupportDescription = [tusSupportDescription stringByAppendingFormat:@" %@", name]; \ + } + AppendTusExtension(OCTUSSupportAvailable, @"available"); + AppendTusExtension(OCTUSSupportExtensionCreation, @"extension:creation"); + AppendTusExtension(OCTUSSupportExtensionCreationWithUpload, @"extension:creation-with-upload"); + AppendTusExtension(OCTUSSupportExtensionExpiration, @"extension:expiration"); + + UInt64 maxChunkSize; + + if ((maxChunkSize = OCTUSInfoGetMaximumSize(_tusInfo)) != 0) + { + tusSupportDescription = [tusSupportDescription stringByAppendingFormat:@" maximumSize:%llu", maxChunkSize]; + } + } + + return ((tusSupportDescription != nil) ? tusSupportDescription : @""); +} + - (NSString *)syncActivityDescription { NSString *activityDescription = nil; @@ -700,7 +745,7 @@ - (NSString *)description { NSString *shareTypesDescription = [self _shareTypesDescription]; - return ([NSString stringWithFormat:@"<%@: %p, type: %lu, name: %@, path: %@, size: %lu bytes, MIME-Type: %@, Last modified: %@, Last used: %@ fileID: %@, eTag: %@, parentID: %@, localID: %@, parentLocalID: %@%@%@%@%@%@%@%@%@%@%@%@>", NSStringFromClass(self.class), self, (unsigned long)self.type, self.name, self.path, self.size, self.mimeType, self.lastModified, self.lastUsed, self.fileID, self.eTag, self.parentFileID, self.localID, self.parentLocalID, ((shareTypesDescription!=nil) ? [NSString stringWithFormat:@", shareTypes: [%@]",shareTypesDescription] : @""), (self.isSharedWithUser ? @", sharedWithUser" : @""), (self.isShareable ? @", shareable" : @""), ((_owner!=nil) ? [NSString stringWithFormat:@", owner: %@", _owner] : @""), (_removed ? @", removed" : @""), (_isFavorite.boolValue ? @", favorite" : @""), (_privateLink ? [NSString stringWithFormat:@", privateLink: %@", _privateLink] : @""), (_checksums ? [NSString stringWithFormat:@", checksums: %@", _checksums] : @""), (_downloadTriggerIdentifier ? [NSString stringWithFormat:@", downloadTrigger: %@", _downloadTriggerIdentifier] : @""), (_fileClaim ? [NSString stringWithFormat:@", fileClaim: %@", _fileClaim] : @""), [self syncActivityDescription]]); + return ([NSString stringWithFormat:@"<%@: %p, type: %lu, name: %@, path: %@, size: %lu bytes, MIME-Type: %@, Last modified: %@, Last used: %@ fileID: %@, eTag: %@, parentID: %@, localID: %@, parentLocalID: %@%@%@%@%@%@%@%@%@%@%@%@%@>", NSStringFromClass(self.class), self, (unsigned long)self.type, self.name, self.path, self.size, self.mimeType, self.lastModified, self.lastUsed, self.fileID, self.eTag, self.parentFileID, self.localID, self.parentLocalID, ((shareTypesDescription!=nil) ? [NSString stringWithFormat:@", shareTypes: [%@]",shareTypesDescription] : @""), (self.isSharedWithUser ? @", sharedWithUser" : @""), (self.isShareable ? @", shareable" : @""), ((_owner!=nil) ? [NSString stringWithFormat:@", owner: %@", _owner] : @""), (_removed ? @", removed" : @""), (_isFavorite.boolValue ? @", favorite" : @""), (_privateLink ? [NSString stringWithFormat:@", privateLink: %@", _privateLink] : @""), (_checksums ? [NSString stringWithFormat:@", checksums: %@", _checksums] : @""), [self _tusSupportDescription], (_downloadTriggerIdentifier ? [NSString stringWithFormat:@", downloadTrigger: %@", _downloadTriggerIdentifier] : @""), (_fileClaim ? [NSString stringWithFormat:@", fileClaim: %@", _fileClaim] : @""), [self syncActivityDescription]]); } #pragma mark - Copying diff --git a/ownCloudSDK/ownCloudSDK.h b/ownCloudSDK/ownCloudSDK.h index 97e497a4..c5f6d515 100644 --- a/ownCloudSDK/ownCloudSDK.h +++ b/ownCloudSDK/ownCloudSDK.h @@ -139,6 +139,8 @@ FOUNDATION_EXPORT const unsigned char ownCloudSDKVersionString[]; #import #import +#import + #import #import #import From f8f100a9f07d6bc61ca79e7e0a8484b800beaf35 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Fri, 1 May 2020 00:19:35 +0200 Subject: [PATCH 2/3] - Add initial TUS support - NSString+TUSMetadata: conversion from dictionary to Upload-Metadata - and back - OCTUSHeader: new class to simplify parsing, building and conversion of TUS-related headers - OCTUSJob: new class to manage the current status of a TUS upload - OCTUSJobSegment: new class to abstract away file segmentation details for TUS uploads - OCConnection: - add new OCConnectionDelegate method to inject/modify TUS capabilities / settings - add new OCConnectionOptionTemporarySegmentFolderURLKey option key to provide a temporary folder to store file segments in when performing uploads via TUS - add code to upload files via TUS where available - OCSyncActionUpload: add OCConnectionOptionTemporarySegmentFolderURLKey option to ensure persistence of temporary segment files - OCEvent: - add OCTUSHeader, OCTUSJob and OCTUSJobSegment to .safeClasses - modernize initWithCoder - OCHTTP: - add OCHTTPStaticHeaderFields type and switch OCHTTPResponse over - add OCHTTPMethodPATCH method --- ownCloudSDK.xcodeproj/project.pbxproj | 18 +- ownCloudSDK/Connection/OCConnection.h | 3 + ownCloudSDK/Connection/OCConnection.m | 519 ++++++++++++++++-- .../Sync/Actions/Upload/OCSyncActionUpload.m | 8 +- ownCloudSDK/Events/OCEvent.m | 15 +- ownCloudSDK/HTTP/OCHTTPTypes.h | 1 + ownCloudSDK/HTTP/Request/OCHTTPRequest.h | 1 + ownCloudSDK/HTTP/Request/OCHTTPRequest.m | 1 + ownCloudSDK/HTTP/Response/OCHTTPResponse.h | 2 +- ownCloudSDK/HTTP/TUS/OCTUSHeader.h | 64 --- ownCloudSDK/HTTP/TUS/OCTUSHeader.m | 88 --- ownCloudSDK/TUS/NSString+TUSMetadata.h | 40 ++ ownCloudSDK/TUS/NSString+TUSMetadata.m | 94 ++++ ownCloudSDK/TUS/OCTUSHeader.h | 91 +++ ownCloudSDK/TUS/OCTUSHeader.m | 280 ++++++++++ ownCloudSDK/TUS/OCTUSJob.h | 71 +++ ownCloudSDK/TUS/OCTUSJob.m | 337 ++++++++++++ ownCloudSDK/ownCloudSDK.h | 1 + 18 files changed, 1426 insertions(+), 208 deletions(-) delete mode 100644 ownCloudSDK/HTTP/TUS/OCTUSHeader.h delete mode 100644 ownCloudSDK/HTTP/TUS/OCTUSHeader.m create mode 100644 ownCloudSDK/TUS/NSString+TUSMetadata.h create mode 100644 ownCloudSDK/TUS/NSString+TUSMetadata.m create mode 100644 ownCloudSDK/TUS/OCTUSHeader.h create mode 100644 ownCloudSDK/TUS/OCTUSHeader.m create mode 100644 ownCloudSDK/TUS/OCTUSJob.h create mode 100644 ownCloudSDK/TUS/OCTUSJob.m diff --git a/ownCloudSDK.xcodeproj/project.pbxproj b/ownCloudSDK.xcodeproj/project.pbxproj index 0281a539..d3909e0c 100644 --- a/ownCloudSDK.xcodeproj/project.pbxproj +++ b/ownCloudSDK.xcodeproj/project.pbxproj @@ -83,6 +83,8 @@ DC19BFEE21CBACBC007C20D1 /* OCProcessManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DC19BFEC21CBACBC007C20D1 /* OCProcessManager.m */; }; DC19BFF121CBE28B007C20D1 /* OCWaitCondition.h in Headers */ = {isa = PBXBuildFile; fileRef = DC19BFEF21CBE28B007C20D1 /* OCWaitCondition.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC19BFF221CBE28B007C20D1 /* OCWaitCondition.m in Sources */ = {isa = PBXBuildFile; fileRef = DC19BFF021CBE28B007C20D1 /* OCWaitCondition.m */; }; + DC1D3DF22459B86200328EBC /* NSString+TUSMetadata.h in Headers */ = {isa = PBXBuildFile; fileRef = DC1D3DF02459B86200328EBC /* NSString+TUSMetadata.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC1D3DF32459B86200328EBC /* NSString+TUSMetadata.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1D3DF12459B86200328EBC /* NSString+TUSMetadata.m */; }; DC1D4D3720DBD58E005A3DFC /* OCFile.h in Headers */ = {isa = PBXBuildFile; fileRef = DC1D4D3520DBD58E005A3DFC /* OCFile.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC1D4D3820DBD58E005A3DFC /* OCFile.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1D4D3620DBD58E005A3DFC /* OCFile.m */; }; DC20DE4B21BFCBC20096000B /* OCLogComponent.h in Headers */ = {isa = PBXBuildFile; fileRef = DC20DE4921BFCBC20096000B /* OCLogComponent.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -492,6 +494,8 @@ DCE26621211348B00001FB2C /* OCCore+CommandLocalImport.m in Sources */ = {isa = PBXBuildFile; fileRef = DCE2661F211348B00001FB2C /* OCCore+CommandLocalImport.m */; }; DCE370942099D18100114981 /* OCDatabaseConsistentOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = DCE370922099D18100114981 /* OCDatabaseConsistentOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCE370952099D18100114981 /* OCDatabaseConsistentOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = DCE370932099D18100114981 /* OCDatabaseConsistentOperation.m */; }; + DCE451A52459AD3F0074363F /* OCTUSJob.h in Headers */ = {isa = PBXBuildFile; fileRef = DCE451A32459AD3F0074363F /* OCTUSJob.h */; }; + DCE451A62459AD3F0074363F /* OCTUSJob.m in Sources */ = {isa = PBXBuildFile; fileRef = DCE451A42459AD3F0074363F /* OCTUSJob.m */; }; DCE48DD8220E1C7B00839E97 /* OCHTTPPipelineTaskCache.h in Headers */ = {isa = PBXBuildFile; fileRef = DCE48DD6220E1C7A00839E97 /* OCHTTPPipelineTaskCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCE48DD9220E1C7B00839E97 /* OCHTTPPipelineTaskCache.m in Sources */ = {isa = PBXBuildFile; fileRef = DCE48DD7220E1C7B00839E97 /* OCHTTPPipelineTaskCache.m */; }; DCE784F922325D4F00733F01 /* OCConnection+Recipients.m in Sources */ = {isa = PBXBuildFile; fileRef = DCE784F722325D4F00733F01 /* OCConnection+Recipients.m */; }; @@ -753,6 +757,8 @@ DC19BFEC21CBACBC007C20D1 /* OCProcessManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCProcessManager.m; sourceTree = ""; }; DC19BFEF21CBE28B007C20D1 /* OCWaitCondition.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCWaitCondition.h; sourceTree = ""; }; DC19BFF021CBE28B007C20D1 /* OCWaitCondition.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCWaitCondition.m; sourceTree = ""; }; + DC1D3DF02459B86200328EBC /* NSString+TUSMetadata.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSString+TUSMetadata.h"; sourceTree = ""; }; + DC1D3DF12459B86200328EBC /* NSString+TUSMetadata.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+TUSMetadata.m"; sourceTree = ""; }; DC1D4D3520DBD58E005A3DFC /* OCFile.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCFile.h; sourceTree = ""; }; DC1D4D3620DBD58E005A3DFC /* OCFile.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCFile.m; sourceTree = ""; }; DC1D4D3D20DC2281005A3DFC /* OCClaim.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCClaim.h; sourceTree = ""; }; @@ -1174,6 +1180,8 @@ DCE2661F211348B00001FB2C /* OCCore+CommandLocalImport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OCCore+CommandLocalImport.m"; sourceTree = ""; }; DCE370922099D18100114981 /* OCDatabaseConsistentOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCDatabaseConsistentOperation.h; sourceTree = ""; }; DCE370932099D18100114981 /* OCDatabaseConsistentOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCDatabaseConsistentOperation.m; sourceTree = ""; }; + DCE451A32459AD3F0074363F /* OCTUSJob.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCTUSJob.h; sourceTree = ""; }; + DCE451A42459AD3F0074363F /* OCTUSJob.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCTUSJob.m; sourceTree = ""; }; DCE48DD6220E1C7A00839E97 /* OCHTTPPipelineTaskCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCHTTPPipelineTaskCache.h; sourceTree = ""; }; DCE48DD7220E1C7B00839E97 /* OCHTTPPipelineTaskCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCHTTPPipelineTaskCache.m; sourceTree = ""; }; DCE784F722325D4F00733F01 /* OCConnection+Recipients.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCConnection+Recipients.m"; sourceTree = ""; }; @@ -1806,7 +1814,6 @@ DC27BBBC230498A4002CC2F8 /* Cookies */, DC7014182209C57B009D4FD9 /* Request */, DC701481220B0865009D4FD9 /* Response */, - DCF39B572458290200DEA137 /* TUS */, ); path = HTTP; sourceTree = ""; @@ -2550,6 +2557,7 @@ DC855707204FEA1500189B9A /* Authentication */, DC4B12002209C24F0062BCDD /* HTTP */, DC855708204FEA3400189B9A /* Connection */, + DCF39B572458290200DEA137 /* TUS */, DC4B11FB220996330062BCDD /* Progress */, DC19BFC621CA67B1007C20D1 /* Issues */, DC1D4D3420DBD557005A3DFC /* File Handling */, @@ -2709,6 +2717,10 @@ children = ( DCF39B542458268E00DEA137 /* OCTUSHeader.m */, DCF39B532458268E00DEA137 /* OCTUSHeader.h */, + DCE451A42459AD3F0074363F /* OCTUSJob.m */, + DCE451A32459AD3F0074363F /* OCTUSJob.h */, + DC1D3DF12459B86200328EBC /* NSString+TUSMetadata.m */, + DC1D3DF02459B86200328EBC /* NSString+TUSMetadata.h */, ); path = TUS; sourceTree = ""; @@ -2827,6 +2839,7 @@ DC179CD520948DF30018DF7F /* NSProgress+OCExtensions.h in Headers */, DCADC0442072CCC900DB8E83 /* OCCoreItemListTask.h in Headers */, DCD3439C2059319C00189B9A /* OCSQLiteDB.h in Headers */, + DC1D3DF22459B86200328EBC /* NSString+TUSMetadata.h in Headers */, DCDD9B18222989E50052A001 /* OCRecipient.h in Headers */, DCADC03F2072774200DB8E83 /* OCQuery+Internal.h in Headers */, DC4A2C5E20D4608100A47260 /* OCIssueChoice.h in Headers */, @@ -2920,6 +2933,7 @@ DC708CE4214135E200FE43CA /* OCSyncActionDownload.h in Headers */, DC188997218B09CC00CFB3F9 /* OCLogFileSource.h in Headers */, DCDD9B14222986D50052A001 /* OCShare+OCXMLObjectCreation.h in Headers */, + DCE451A52459AD3F0074363F /* OCTUSJob.h in Headers */, DC42254720766D63002E01E4 /* OCReachabilityMonitor.h in Headers */, DCDD9B2B22312ED80052A001 /* OCRateLimiter.h in Headers */, DC3CE0482429FCDF00AB8B88 /* OCMessageQueue.h in Headers */, @@ -3357,6 +3371,7 @@ DC114A9522A7A87C00CBD597 /* NSData+OCRandom.m in Sources */, DC8556EE204DEA2900189B9A /* OCHTTPDAVRequest.m in Sources */, DC4AFAA7206A6E7100189B9A /* OCSQLiteResultSet.m in Sources */, + DCE451A62459AD3F0074363F /* OCTUSJob.m in Sources */, DC19BFEE21CBACBC007C20D1 /* OCProcessManager.m in Sources */, DCC8FA0020285C1500EB6701 /* OCAuthenticationMethodOAuth2.m in Sources */, DCDD9B15222986D50052A001 /* OCShare+OCXMLObjectCreation.m in Sources */, @@ -3393,6 +3408,7 @@ DC2F6376223A61990063C2DA /* OCCoreQuery.m in Sources */, DCC8F9F7202855A200EB6701 /* OCShare.m in Sources */, DC39DC472041A03300189B9A /* OCAuthenticationMethodBasicAuth.m in Sources */, + DC1D3DF32459B86200328EBC /* NSString+TUSMetadata.m in Sources */, DC34227B217CAA0B00705508 /* OCIPNotificationCenter.m in Sources */, DCC8FA0C2029C0BE00EB6701 /* OCQueryFilter.m in Sources */, DCDBEE2C2048A6A800189B9A /* OCConnection+Setup.m in Sources */, diff --git a/ownCloudSDK/Connection/OCConnection.h b/ownCloudSDK/Connection/OCConnection.h index be18d4b3..a4a60d63 100644 --- a/ownCloudSDK/Connection/OCConnection.h +++ b/ownCloudSDK/Connection/OCConnection.h @@ -73,6 +73,8 @@ NS_ASSUME_NONNULL_BEGIN - (OCHTTPRequestInstruction)connection:(OCConnection *)connection instructionForFinishedRequest:(OCHTTPRequest *)request withResponse:(OCHTTPResponse *)response error:(NSError *)error defaultsTo:(OCHTTPRequestInstruction)defaultInstruction; +- (nullable OCTUSHeader *)connection:(OCConnection *)connection tusHeader:(nullable OCTUSHeader *)tusHeader forChildrenOf:(OCItem *)parentItem; + @end NS_ASSUME_NONNULL_END @@ -383,6 +385,7 @@ extern OCConnectionOptionKey OCConnectionOptionChecksumKey; //!< OCChecksum inst extern OCConnectionOptionKey OCConnectionOptionChecksumAlgorithmKey; //!< OCChecksumAlgorithmIdentifier identifying the checksum algorithm to use to compute checksums for the "OC-Checksum" header in uploads extern OCConnectionOptionKey OCConnectionOptionGroupIDKey; //!< OCHTTPRequestGroupID to use for requests extern OCConnectionOptionKey OCConnectionOptionRequiredSignalsKey; //!< NSSet with the signal ids to require for the requests +extern OCConnectionOptionKey OCConnectionOptionTemporarySegmentFolderURLKey; //!< NSURL of the temporary folder to store file segments in when performing uploads via TUS extern OCIPCNotificationName OCIPCNotificationNameConnectionSettingsChanged; //!< Posted when connection settings changed diff --git a/ownCloudSDK/Connection/OCConnection.m b/ownCloudSDK/Connection/OCConnection.m index f3387bee..fdbebe1e 100644 --- a/ownCloudSDK/Connection/OCConnection.m +++ b/ownCloudSDK/Connection/OCConnection.m @@ -42,6 +42,7 @@ #import "NSString+OCPath.h" #import "OCMacros.h" #import "OCCore.h" +#import "OCTUSJob.h" // Imported to use the identifiers in OCConnectionPreferredAuthenticationMethodIDs only #import "OCAuthenticationMethodOpenIDConnect.h" @@ -1371,8 +1372,7 @@ - (OCHTTPPipeline *)transferPipelineForRequest:(OCHTTPRequest *)request withExpe #pragma mark - File transfer: upload - (OCProgress *)uploadFileFromURL:(NSURL *)sourceURL withName:(NSString *)fileName to:(OCItem *)newParentDirectory replacingItem:(OCItem *)replacedItem options:(NSDictionary *)options resultTarget:(OCEventTarget *)eventTarget { - OCProgress *requestProgress = nil; - NSURL *uploadURL; + // OCProgress *requestProgress = nil; if ((sourceURL == nil) || (newParentDirectory == nil)) { @@ -1398,84 +1398,502 @@ - (OCProgress *)uploadFileFromURL:(NSURL *)sourceURL withName:(NSString *)fileNa return(nil); } - if ((uploadURL = [[[self URLForEndpoint:OCConnectionEndpointIDWebDAVRoot options:nil] URLByAppendingPathComponent:newParentDirectory.path] URLByAppendingPathComponent:fileName]) != nil) + // Determine file size + NSNumber *fileSize = nil; { - OCHTTPRequest *request = [OCHTTPRequest requestWithURL:uploadURL]; + NSError *error = nil; + if (![sourceURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:&error]) + { + OCLogError(@"Error determining size of %@: %@", sourceURL, error); + } + } - request.method = OCHTTPMethodPUT; + // Determine modification date + NSDate *modDate = nil; + if ((modDate = options[OCConnectionOptionLastModificationDateKey]) == nil) + { + NSError *error = nil; - // Set Content-Type - [request setValue:@"application/octet-stream" forHeaderField:@"Content-Type"]; + if (![sourceURL getResourceValue:&modDate forKey:NSURLAttributeModificationDateKey error:NULL]) + { + OCLogError(@"Error determining modification date of %@: %@", sourceURL, error); + modDate = nil; + } + } - // Set conditions - if (replacedItem != nil) + // Compute checksum + __block OCChecksum *checksum = nil; + + if ((checksum = options[OCConnectionOptionChecksumKey]) == nil) + { + OCChecksumAlgorithmIdentifier checksumAlgorithmIdentifier = options[OCConnectionOptionChecksumAlgorithmKey]; + + if (checksumAlgorithmIdentifier==nil) { - // Ensure the upload fails if there's a different version at the target already - [request setValue:replacedItem.eTag forHeaderField:@"If-Match"]; + checksumAlgorithmIdentifier = _preferredChecksumAlgorithm; } - else + + OCSyncExec(checksumComputation, { + [OCChecksum computeForFile:sourceURL checksumAlgorithm:checksumAlgorithmIdentifier completionHandler:^(NSError *error, OCChecksum *computedChecksum) { + checksum = computedChecksum; + OCSyncExecDone(checksumComputation); + }]; + }); + } + + if ((sourceURL == nil) || (fileName == nil) || (newParentDirectory == nil) || (modDate == nil) || (fileSize == nil)) + { + [eventTarget handleError:OCError(OCErrorInsufficientParameters) type:OCEventTypeUpload uuid:nil sender:self]; + return(nil); + } + + // Determine TUS info + OCTUSHeader *parentTusHeader = nil; + + if (OCTUSIsAvailable(newParentDirectory.tusSupport)) + { + // Instantiate from OCItem + parentTusHeader = [[OCTUSHeader alloc] initWithTUSInfo:newParentDirectory.tusInfo]; + } + + if ((_delegate != nil) && ([_delegate respondsToSelector:@selector(connection:tusHeader:forChildrenOf:)])) + { + // Modify / Retrieve from delegate + parentTusHeader = [_delegate connection:self tusHeader:parentTusHeader forChildrenOf:newParentDirectory]; + } + + // Start upload + if ((parentTusHeader != nil) && OCTUSIsAvailable(parentTusHeader.supportFlags) && // TUS support available + OCTUSIsSupported(parentTusHeader.supportFlags, OCTUSSupportExtensionCreation)) // TUS creation extension available + { + // Use TUS + return ([self _tusUploadFileFromURL:sourceURL withName:fileName modificationDate:modDate fileSize:fileSize checksum:checksum tusHeader:parentTusHeader to:newParentDirectory replacingItem:replacedItem options:options resultTarget:eventTarget]); + } + else + { + // Use a single "traditional" PUT for uploads + return ([self _directUploadFileFromURL:sourceURL withName:fileName modificationDate:modDate fileSize:fileSize checksum:checksum to:newParentDirectory replacingItem:replacedItem options:options resultTarget:eventTarget]); + } +} + +#pragma mark - File transfer: resumable upload (TUS) +- (OCProgress *)_tusUploadFileFromURL:(NSURL *)sourceURL withName:(NSString *)fileName modificationDate:(NSDate *)modificationDate fileSize:(NSNumber *)fileSize checksum:(OCChecksum *)checksum tusHeader:(OCTUSHeader *)parentTusHeader to:(OCItem *)parentItem replacingItem:(OCItem *)replacedItem options:(NSDictionary *)options resultTarget:(OCEventTarget *)eventTarget +{ + OCProgress *tusProgress = nil; + NSURL *segmentFolderURL = options[OCConnectionOptionTemporarySegmentFolderURLKey]; + NSError *error = nil; + + // Determine segment folder + if (segmentFolderURL == nil) + { + segmentFolderURL = [[NSURL fileURLWithPath:NSTemporaryDirectory()] URLByAppendingPathComponent:[NSString stringWithFormat:@"OCTUS-%@",NSUUID.UUID.UUIDString]]; + } + + if (segmentFolderURL == nil) + { + [eventTarget handleError:OCError(OCErrorInsufficientStorage) type:OCEventTypeUpload uuid:nil sender:self]; + return(nil); + } + else + { + if (![[NSFileManager defaultManager] createDirectoryAtURL:segmentFolderURL withIntermediateDirectories:YES attributes:@{ NSFileProtectionKey : NSFileProtectionCompleteUntilFirstUserAuthentication } error:&error]) { - // Ensure the upload fails if there's any file at the target already - [request setValue:@"*" forHeaderField:@"If-None-Match"]; + segmentFolderURL = nil; + OCLogError(@"Error creating TUS segment folder at %@: %@", segmentFolderURL, error); } + } - // Set Content-Length - NSNumber *fileSize = nil; - if ([sourceURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL]) + // Clone source file to segment folder + NSURL *clonedSourceURL = [segmentFolderURL URLByAppendingPathComponent:sourceURL.lastPathComponent isDirectory:NO]; + + if (![NSFileManager.defaultManager copyItemAtURL:sourceURL toURL:clonedSourceURL error:&error]) + { + OCLogError(@"Error cloning sourceURL %@ to segment folder at %@: %@", sourceURL, segmentFolderURL, error); + [eventTarget handleError:OCError(OCErrorInsufficientStorage) type:OCEventTypeUpload uuid:nil sender:self]; + return(nil); + } + + // Create TUS job + OCTUSJob *tusJob; + NSURL *creationURL = [[self URLForEndpoint:OCConnectionEndpointIDWebDAVRoot options:nil] URLByAppendingPathComponent:parentItem.path]; + + if ((tusJob = [[OCTUSJob alloc] initWithHeader:parentTusHeader segmentFolderURL:segmentFolderURL fileURL:clonedSourceURL creationURL:creationURL]) != nil) + { + tusJob.fileName = fileName; + tusJob.fileSize = fileSize; + tusJob.fileModDate = modificationDate; + tusJob.fileChecksum = checksum; + + tusJob.futureItemPath = [parentItem.path stringByAppendingPathComponent:fileName]; + + tusJob.eventTarget = eventTarget; + + tusJob.maxSegmentSize = 111000; // TODO: here for testing only, remove + + tusProgress = [self _continueTusJob:tusJob lastTask:nil]; + } + + return (tusProgress); +} + +- (OCProgress *)_continueTusJob:(OCTUSJob *)tusJob lastTask:(NSString *)lastTask +{ + OCProgress *tusProgress = nil; + BOOL useCreationWithUpload = NO; // OCTUSIsSupported(tusJob.header.supportFlags, OCTUSSupportExtensionCreationWithUpload); + OCHTTPRequest *request = nil; + + /* + OCTUSJob handling flow: + + Q: Is there an .uploadURL? + + 1) No -> upload has not yet started + - initiate upload via create or create-with-upload + - response provides Tus-Resumable header + - response status indicates success and provides Location: + - save the URL returned via the Location header as .uploadURL + - save 0 or $numberOfBytesAlreadyUploaded for .uploadOffset + -> _continueTusJob… + + - response indicates failure + - partial response with Location header + - save Location header to .uploadURL + - set nil for .uploadOffset + -> _continueTusJob… + - no response / response without Location heaer + - restart upload via create or create-with-upload if necessary / makes sense (-> _continueTusJob…) + - stop upload with error otherwise (-> message .eventTarget) + + - response provides NO Tus-Resumable header + -> Tus not supported, reschedule as "traditional" direct upload + + 2) Yes -> continue upload + - is .uploadOffset set? + - no + - send HEAD request to .uploadURL to determine current Upload-Offset. On return: + - response contains "Upload-Offset" + - use as value for .uploadOffset + -> _continueTusJob… + - response doesn't contain "Upload-Offset" + - if status is 404 -> upload was removed + - set .uploadURL to nil to trigger an upload restart + -> _continueTusJob… + - other errors + - stop upload with error + -> message .eventTarget + + - yes + - POST the next segment to .uploadURL + - on success + - increment .uploadOffset by the size of the segment + - are any segments left? + - yes + -> _continueTusJob… + - no + -> initiate targeted PROPFIND and message .eventTarget + - on failure + - set .uploadOffset to nil + -> _continueTusJob… + + */ + + OCTUSHeader *reqTusHeader = [OCTUSHeader new]; + reqTusHeader.version = @"1.0.0"; + + if (tusJob.uploadURL == nil) + { + // Create file for upload and determine upload URL + request = [OCHTTPRequest requestWithURL:tusJob.creationURL]; + request.method = OCHTTPMethodPOST; + + // Compose header + reqTusHeader.uploadLength = tusJob.fileSize; + reqTusHeader.uploadMetadata = @{ + OCTUSMetadataKeyFileName : tusJob.fileName, + OCTUSMetadataKeyChecksum : [NSString stringWithFormat:@"%@ %@", tusJob.fileChecksum.algorithmIdentifier, tusJob.fileChecksum.checksum] + }; + + if (useCreationWithUpload) { - OCLogDebug(@"Uploading file %@ (%@ bytes)..", OCLogPrivate(fileName), fileSize); - [request setValue:fileSize.stringValue forHeaderField:@"Content-Length"]; + reqTusHeader.uploadOffset = @(0); + [request setValue:@"application/offset+octet-stream" forHeaderField:@"Content-Type"]; + + // request.bodyURL = sourceURL; } - // Set modification date - NSDate *modDate = nil; - if ((modDate = options[OCConnectionOptionLastModificationDateKey]) == nil) + [request addHeaderFields:reqTusHeader.httpHeaderFields]; + + // TODO: clarify if conditions (If-Match / If-None-Match) are still relevant/supported with ocis + + // Add userInfo + request.userInfo = @{ + @"task" : @"create", + @"job" : tusJob + }; + } + else + { + if (tusJob.uploadOffset == nil) + { + // Determine .uploadOffset + request = [OCHTTPRequest requestWithURL:tusJob.uploadURL]; + request.method = OCHTTPMethodHEAD; + + // Compose header + [request addHeaderFields:reqTusHeader.httpHeaderFields]; + + // Add userInfo + request.userInfo = @{ + @"task" : @"head", + @"job" : tusJob + }; + } + else + { + if (tusJob.uploadOffset.unsignedIntegerValue == tusJob.fileSize.unsignedIntegerValue) + { + // Upload complete + + // Destroy TusJob + [tusJob destroy]; + + // Retrieve item information + [self retrieveItemListAtPath:tusJob.futureItemPath depth:0 options:@{ + @"alternativeEventType" : @(OCEventTypeUpload), + } resultTarget:tusJob.eventTarget]; + } + else + { + // Continue upload from .uploadOffset + request = [OCHTTPRequest requestWithURL:tusJob.uploadURL]; + request.method = OCHTTPMethodPATCH; + + // Compose body + NSError *error; + OCTUSJobSegment *segment = [tusJob requestSegmentFromOffset:tusJob.uploadOffset.unsignedIntegerValue + withSize:((tusJob.maxSegmentSize == 0) ? + (tusJob.fileSize.unsignedIntegerValue - tusJob.uploadOffset.unsignedIntegerValue) : + tusJob.maxSegmentSize + ) + error:&error]; + + if (segment != nil) + { + request.bodyURL = segment.url; + } + + // Compose header + reqTusHeader.uploadOffset = tusJob.uploadOffset; + // reqTusHeader.uploadLength = @(segment.size); + [request addHeaderFields:reqTusHeader.httpHeaderFields]; + [request setValue:@"application/offset+octet-stream" forHeaderField:@"Content-Type"]; + + // Add userInfo + request.userInfo = @{ + @"task" : @"upload", + @"job" : tusJob, + @"segmentSize" : @(segment.size) + }; + } + } + } + + if (request != nil) + { + // Set meta data for handling + request.requiredSignals = self.actionSignals; + request.resultHandlerAction = @selector(_handleUploadTusJobResult:error:); + request.eventTarget = tusJob.eventTarget; + request.forceCertificateDecisionDelegation = YES; + + // Attach to pipelines + [self attachToPipelines]; + + // Enqueue request +// if (options[OCConnectionOptionRequestObserverKey] != nil) +// { +// request.requestObserver = options[OCConnectionOptionRequestObserverKey]; +// } + + [[self transferPipelineForRequest:request withExpectedResponseLength:1000] enqueueRequest:request forPartitionID:self.partitionID]; + } + + return (tusProgress); +} + +- (void)_handleUploadTusJobResult:(OCHTTPRequest *)request error:(NSError *)error +{ + NSString *task = request.userInfo[@"task"]; + OCTUSJob *tusJob = request.userInfo[@"job"]; + BOOL isTusResponse = (request.httpResponse.headerFields[OCTUSHeaderNameTusResumable] != nil); // Tus-Resumable header indicates server supports TUS + + if ([task isEqual:@"create"]) + { + NSString *location = request.httpResponse.headerFields[@"Location"]; // URL to continue the upload at + + #warning remove this hack + location = [location stringByReplacingOccurrencesOfString:@"localhost" withString:tusJob.creationURL.host]; + + if (isTusResponse && (location != nil)) { - if (![sourceURL getResourceValue:&modDate forKey:NSURLAttributeModificationDateKey error:NULL]) + if (request.httpResponse.status.isSuccess) // Expected: 201 Created { - modDate = nil; + tusJob.uploadURL = [NSURL URLWithString:location]; // save Location to .uploadURL + + if (error == nil) + { + tusJob.uploadOffset = @(0); // TODO: revise when adding support for create-with-upload + } + else + { + tusJob.uploadOffset = nil; // ensure a HEAD request is sent to determine current upload status before continuing + } + + // Continue + [self _continueTusJob:tusJob lastTask:task]; + } + else + { + // Stop upload with an error + OCTLogError(@[@"TUS"], @"creation response doesn't indicate success"); + [self _errorEventFromRequest:request error:error send:YES]; } } - if (modDate != nil) + else { - [request setValue:[@((SInt64)[modDate timeIntervalSince1970]) stringValue] forHeaderField:@"X-OC-MTime"]; + // Stop upload with an error + OCTLogError(@[@"TUS"], @"creation response is not a TUS response"); + [self _errorEventFromRequest:request error:error send:YES]; } + } + else if ([task isEqual:@"head"]) + { + if (isTusResponse && + request.httpResponse.status.isSuccess && // Expected: 200 OK + (request.httpResponse.headerFields != nil)) + { + OCTUSHeader *tusHeader = [[OCTUSHeader alloc] initWithHTTPHeaderFields:request.httpResponse.headerFields]; - // Compute and set checksum header - OCChecksumHeaderString checksumHeaderValue = nil; - __block OCChecksum *checksum = nil; + if (tusHeader.uploadOffset != nil) + { + OCTLogDebug(@[@"TUS"], @"TUS HEAD response indicates uploadOffset of %@", tusHeader.uploadOffset); - if ((checksum = options[OCConnectionOptionChecksumKey]) == nil) + tusJob.uploadOffset = tusHeader.uploadOffset; + [self _continueTusJob:tusJob lastTask:task]; + } + else + { + OCTLogError(@[@"TUS"], @"TUS HEAD response lacks expected Upload-Offset"); + [request.eventTarget handleError:OCError(OCErrorResponseUnknownFormat) type:OCEventTypeUpload uuid:nil sender:self]; + } + } + else { - OCChecksumAlgorithmIdentifier checksumAlgorithmIdentifier = options[OCConnectionOptionChecksumAlgorithmKey]; + // Stop upload with an error + [self _errorEventFromRequest:request error:error send:YES]; + } + } + else if ([task isEqual:@"upload"]) + { + if (isTusResponse && + request.httpResponse.status.isSuccess && // Expected: 204 No Content + (request.httpResponse.headerFields != nil)) + { + OCTUSHeader *tusHeader = [[OCTUSHeader alloc] initWithHTTPHeaderFields:request.httpResponse.headerFields]; - if (checksumAlgorithmIdentifier==nil) + if (tusHeader.uploadOffset != nil) { - checksumAlgorithmIdentifier = _preferredChecksumAlgorithm; + OCTLogDebug(@[@"TUS"], @"TUS upload response indicates uploadOffset of %@", tusHeader.uploadOffset); + + tusJob.uploadOffset = tusHeader.uploadOffset; + [self _continueTusJob:tusJob lastTask:task]; } + } + } +} - OCSyncExec(checksumComputation, { - [OCChecksum computeForFile:sourceURL checksumAlgorithm:checksumAlgorithmIdentifier completionHandler:^(NSError *error, OCChecksum *computedChecksum) { - checksum = computedChecksum; - OCSyncExecDone(checksumComputation); - }]; - }); +- (OCEvent *)_errorEventFromRequest:(OCHTTPRequest *)request error:(NSError *)error send:(BOOL)send +{ + OCEvent *event; + + if ((event = [OCEvent eventForEventTarget:request.eventTarget type:OCEventTypeUpload uuid:request.identifier attributes:nil]) != nil) + { + if (error != nil) + { + event.error = error; + } + else + { + if (request.error != nil) + { + event.error = request.error; + } + else + { + event.error = request.httpResponse.status.error; + } } - if ((checksum != nil) && ((checksumHeaderValue = checksum.headerString) != nil)) + // Add date to error + if (event.error != nil) { - [request setValue:checksumHeaderValue forHeaderField:@"OC-Checksum"]; + OCErrorAddDateFromResponse(event.error, request.httpResponse); + } + + if (send) + { + [request.eventTarget handleEvent:event sender:self]; + } + } + + return (event); +} + +#pragma mark - File transfer: direct upload (PUT) +- (OCProgress *)_directUploadFileFromURL:(NSURL *)sourceURL withName:(NSString *)fileName modificationDate:(NSDate *)modDate fileSize:(NSNumber *)fileSize checksum:(OCChecksum *)checksum to:(OCItem *)newParentDirectory replacingItem:(OCItem *)replacedItem options:(NSDictionary *)options resultTarget:(OCEventTarget *)eventTarget +{ + OCProgress *requestProgress = nil; + NSURL *uploadURL; + + if ((uploadURL = [[[self URLForEndpoint:OCConnectionEndpointIDWebDAVRoot options:nil] URLByAppendingPathComponent:newParentDirectory.path] URLByAppendingPathComponent:fileName]) != nil) + { + OCHTTPRequest *request = [OCHTTPRequest requestWithURL:uploadURL]; + + request.method = OCHTTPMethodPUT; + + // Set Content-Type + [request setValue:@"application/octet-stream" forHeaderField:@"Content-Type"]; + + // Set conditions + if (replacedItem != nil) + { + // Ensure the upload fails if there's a different version at the target already + [request setValue:replacedItem.eTag forHeaderField:@"If-Match"]; } + else + { + // Ensure the upload fails if there's any file at the target already + [request setValue:@"*" forHeaderField:@"If-None-Match"]; + } + + // Set Content-Length + OCLogDebug(@"Uploading file %@ (%@ bytes)..", OCLogPrivate(fileName), fileSize); + [request setValue:fileSize.stringValue forHeaderField:@"Content-Length"]; + + // Set modification date + [request setValue:[@((SInt64)[modDate timeIntervalSince1970]) stringValue] forHeaderField:@"X-OC-MTime"]; - if ((sourceURL == nil) || (fileName == nil) || (newParentDirectory == nil) || (modDate == nil) || (fileSize == nil)) + // Set checksum header + OCChecksumHeaderString checksumHeaderValue = nil; + + if ((checksum != nil) && ((checksumHeaderValue = checksum.headerString) != nil)) { - [eventTarget handleError:OCError(OCErrorInsufficientParameters) type:OCEventTypeUpload uuid:nil sender:self]; - return(nil); + [request setValue:checksumHeaderValue forHeaderField:@"OC-Checksum"]; } // Set meta data for handling request.requiredSignals = self.actionSignals; - request.resultHandlerAction = @selector(_handleUploadFileResult:error:); + request.resultHandlerAction = @selector(_handleDirectUploadFileResult:error:); request.userInfo = @{ @"sourceURL" : sourceURL, @"fileName" : fileName, @@ -1512,6 +1930,12 @@ - (OCProgress *)uploadFileFromURL:(NSURL *)sourceURL withName:(NSString *)fileNa } - (void)_handleUploadFileResult:(OCHTTPRequest *)request error:(NSError *)error +{ + // Compatibility with previous selector (from before the addition of TUS support) + [self _handleDirectUploadFileResult:request error:error]; +} + +- (void)_handleDirectUploadFileResult:(OCHTTPRequest *)request error:(NSError *)error { NSString *fileName = request.userInfo[@"fileName"]; OCItem *parentItem = request.userInfo[@"parentItem"]; @@ -1545,10 +1969,10 @@ - (void)_handleUploadFileResult:(OCHTTPRequest *)request error:(NSError *)error } */ - // Retrieve item information and continue in _handleUploadFileItemResult:error: + // Retrieve item information [self retrieveItemListAtPath:[parentItem.path stringByAppendingPathComponent:fileName] depth:0 options:@{ @"alternativeEventType" : @(OCEventTypeUpload), - @"_originalUserInfo" : request.userInfo + // @"_originalUserInfo" : request.userInfo } resultTarget:request.eventTarget]; } else @@ -2682,6 +3106,7 @@ - (NSError *)sendSynchronousRequest:(OCHTTPRequest *)request OCConnectionOptionKey OCConnectionOptionChecksumAlgorithmKey = @"checksum-algorithm"; OCConnectionOptionKey OCConnectionOptionGroupIDKey = @"group-id"; OCConnectionOptionKey OCConnectionOptionRequiredSignalsKey = @"required-signals"; +OCConnectionOptionKey OCConnectionOptionTemporarySegmentFolderURLKey = @"temporary-segment-folder-url"; OCIPCNotificationName OCIPCNotificationNameConnectionSettingsChanged = @"org.owncloud.connection-settings-changed"; diff --git a/ownCloudSDK/Core/Sync/Actions/Upload/OCSyncActionUpload.m b/ownCloudSDK/Core/Sync/Actions/Upload/OCSyncActionUpload.m index bb0e1bf6..ca88f731 100644 --- a/ownCloudSDK/Core/Sync/Actions/Upload/OCSyncActionUpload.m +++ b/ownCloudSDK/Core/Sync/Actions/Upload/OCSyncActionUpload.m @@ -175,11 +175,15 @@ - (OCCoreSyncInstruction)scheduleWithContext:(OCSyncContext *)syncContext }]; }); + // Create segment folder + NSURL *segmentFolderURL = [[self.core.vault.rootURL URLByAppendingPathComponent:@"TUS"] URLByAppendingPathComponent:NSUUID.UUID.UUIDString]; + // Schedule the upload NSDate *lastModificationDate = ((uploadItem.lastModified != nil) ? uploadItem.lastModified : [NSDate new]); NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: - lastModificationDate, OCConnectionOptionLastModificationDateKey, - self.importFileChecksum, OCConnectionOptionChecksumKey, // not using @{} syntax here: if importFileChecksum is nil for any reason, that'd throw + segmentFolderURL, OCConnectionOptionTemporarySegmentFolderURLKey, + lastModificationDate, OCConnectionOptionLastModificationDateKey, + self.importFileChecksum, OCConnectionOptionChecksumKey, // not using @{} syntax here: if importFileChecksum is nil for any reason, that'd throw nil]; [self setupProgressSupportForItem:self.latestVersionOfLocalItem options:&options syncContext:syncContext]; diff --git a/ownCloudSDK/Events/OCEvent.m b/ownCloudSDK/Events/OCEvent.m index 21519903..6188ea5d 100644 --- a/ownCloudSDK/Events/OCEvent.m +++ b/ownCloudSDK/Events/OCEvent.m @@ -44,6 +44,8 @@ #import "OCSyncIssueChoice.h" #import "OCUser.h" #import "OCWaitCondition.h" +#import "OCTUSJob.h" +#import "OCTUSHeader.h" @implementation OCEvent @@ -99,6 +101,9 @@ @implementation OCEvent OCSyncIssueChoice.class, OCUser.class, OCWaitCondition.class, + OCTUSHeader.class, + OCTUSJob.class, + OCTUSJobSegment.class, // Foundation classes NSArray.class, @@ -257,19 +262,19 @@ - (instancetype)initWithCoder:(NSCoder *)decoder { if ((self = [super init]) != nil) { - _uuid = [decoder decodeObjectOfClass:[NSString class] forKey:@"uuid"]; + _uuid = [decoder decodeObjectOfClass:NSString.class forKey:@"uuid"]; _eventType = [decoder decodeIntegerForKey:@"eventType"]; _userInfo = [decoder decodeObjectOfClasses:OCEvent.safeClasses forKey:@"userInfo"]; - _path = [decoder decodeObjectOfClass:[NSString class] forKey:@"path"]; + _path = [decoder decodeObjectOfClass:NSString.class forKey:@"path"]; _depth = [decoder decodeIntegerForKey:@"depth"]; - _mimeType = [decoder decodeObjectOfClass:[NSString class] forKey:@"mimeType"]; + _mimeType = [decoder decodeObjectOfClass:NSString.class forKey:@"mimeType"]; _file = [decoder decodeObjectOfClass:OCFile.class forKey:@"file"]; - _error = [decoder decodeObjectOfClass:[NSError class] forKey:@"error"]; - _result = [decoder decodeObjectOfClasses:[OCEvent safeClasses] forKey:@"result"]; + _error = [decoder decodeObjectOfClass:NSError.class forKey:@"error"]; + _result = [decoder decodeObjectOfClasses:OCEvent.safeClasses forKey:@"result"]; } return (self); diff --git a/ownCloudSDK/HTTP/OCHTTPTypes.h b/ownCloudSDK/HTTP/OCHTTPTypes.h index 621c4899..df6b0785 100644 --- a/ownCloudSDK/HTTP/OCHTTPTypes.h +++ b/ownCloudSDK/HTTP/OCHTTPTypes.h @@ -23,6 +23,7 @@ NS_ASSUME_NONNULL_BEGIN typedef NSString* OCHTTPMethod NS_TYPED_ENUM; +typedef NSDictionary* OCHTTPStaticHeaderFields; typedef NSMutableDictionary* OCHTTPHeaderFields; typedef NSMutableDictionary* OCHTTPRequestParameters; diff --git a/ownCloudSDK/HTTP/Request/OCHTTPRequest.h b/ownCloudSDK/HTTP/Request/OCHTTPRequest.h index a0cd7909..a412a846 100644 --- a/ownCloudSDK/HTTP/Request/OCHTTPRequest.h +++ b/ownCloudSDK/HTTP/Request/OCHTTPRequest.h @@ -122,6 +122,7 @@ typedef NSDictionary* OCHTTPRequestResumeInfo; extern OCHTTPMethod OCHTTPMethodGET; extern OCHTTPMethod OCHTTPMethodPOST; +extern OCHTTPMethod OCHTTPMethodPATCH; extern OCHTTPMethod OCHTTPMethodHEAD; extern OCHTTPMethod OCHTTPMethodPUT; extern OCHTTPMethod OCHTTPMethodDELETE; diff --git a/ownCloudSDK/HTTP/Request/OCHTTPRequest.m b/ownCloudSDK/HTTP/Request/OCHTTPRequest.m index d43db67c..2b4fb385 100644 --- a/ownCloudSDK/HTTP/Request/OCHTTPRequest.m +++ b/ownCloudSDK/HTTP/Request/OCHTTPRequest.m @@ -467,6 +467,7 @@ - (void)encodeWithCoder:(NSCoder *)coder OCHTTPMethod OCHTTPMethodGET = @"GET"; OCHTTPMethod OCHTTPMethodPOST = @"POST"; +OCHTTPMethod OCHTTPMethodPATCH = @"PATCH"; OCHTTPMethod OCHTTPMethodHEAD = @"HEAD"; OCHTTPMethod OCHTTPMethodPUT = @"PUT"; OCHTTPMethod OCHTTPMethodDELETE = @"DELETE"; diff --git a/ownCloudSDK/HTTP/Response/OCHTTPResponse.h b/ownCloudSDK/HTTP/Response/OCHTTPResponse.h index 18ceb36b..454b2c03 100644 --- a/ownCloudSDK/HTTP/Response/OCHTTPResponse.h +++ b/ownCloudSDK/HTTP/Response/OCHTTPResponse.h @@ -39,7 +39,7 @@ NS_ASSUME_NONNULL_BEGIN @property(strong,nullable) NSError *certificateValidationError; //!< Any error that occured during validation of the certificate (if any). @property(strong,nullable) OCHTTPStatus *status; //!< The HTTP status returned by the server -@property(strong,nullable) OCHTTPHeaderFields headerFields; //!< All HTTP header fields +@property(strong,nullable) OCHTTPStaticHeaderFields headerFields;//!< All HTTP header fields @property(strong,nullable,nonatomic) NSHTTPURLResponse *httpURLResponse; //!< The NSHTTPURLResponse returned by the server. If set, is used to populate httpStatus and allHTTPHeaderFields. diff --git a/ownCloudSDK/HTTP/TUS/OCTUSHeader.h b/ownCloudSDK/HTTP/TUS/OCTUSHeader.h deleted file mode 100644 index 4f882ac6..00000000 --- a/ownCloudSDK/HTTP/TUS/OCTUSHeader.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// OCTUSHeader.h -// ownCloudSDK -// -// Created by Felix Schwarz on 28.04.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. -// - -#import -#import "OCHTTPTypes.h" - -NS_ASSUME_NONNULL_BEGIN - -typedef NSString* OCTUSVersion; -typedef NSString* OCTUSExtension; - -typedef NSString* OCTUSHeaderJSON; - -typedef NS_OPTIONS(UInt8, OCTUSSupport) -{ - OCTUSSupportNone, - OCTUSSupportAvailable = (1<<0), - OCTUSSupportExtensionCreation = (1<<1), - OCTUSSupportExtensionCreationWithUpload = (1<<2), - OCTUSSupportExtensionExpiration = (1<<3) -}; - -typedef struct { - UInt8 reserved : 8; - UInt64 maximumSize : 48; - OCTUSSupport tusSupport : 8; -} OCTUSInfoPrivate; - -typedef UInt64 OCTUSInfo; // encodes OCTUSSupport, maxSize and more - -#define OCTUSInfoGetSupport(info) ((OCTUSInfoPrivate *)&info)->tusSupport -#define OCTUSInfoSetSupport(info,flags) ((OCTUSInfoPrivate *)&info)->tusSupport = (flags) - -#define OCTUSInfoGetMaximumSize(info) ((OCTUSInfoPrivate *)&info)->maximumSize -#define OCTUSInfoSetMaximumSize(info,maxSize) ((OCTUSInfoPrivate *)&info)->maximumSize = (maxSize) - -@interface OCTUSHeader : NSObject // - -@property(strong,nullable) OCTUSVersion version; //!< Corresponds to "Tus-Resumable" -@property(strong,nullable) NSArray *versions; //!< Corresponds to "Tus-Version" header (where available), with fallback to "Tus-Resumable" -@property(strong,nullable) NSArray *extensions; //!< Corresponds to "Tus-Extension" header - -@property(strong,nullable) NSNumber *maximumSize; //!< Corresponds to "Tus-Max-Size" header - -@property(strong,nullable) NSNumber *uploadOffset; //!< Corresponds to "Upload-Offset" header -@property(strong,nullable) NSNumber *uploadLength; //!< Corresponds to "Upload-Length" header - -@property(readonly,nonatomic) OCTUSSupport supportFlags; //!< Returns TUS support info compressed as set of flags -@property(readonly,nonatomic) OCTUSInfo info; //!< Returns TUS info compressed to an integer - -- (instancetype)initWithHTTPHeaderFields:(OCHTTPHeaderFields)headerFields; -//- (OCHTTPHeaderFields)httpHeaderFields; - -//- (instancetype)initWithHeaderJSON:(OCTUSHeaderJSON)headerJSON; -//- (OCTUSHeaderJSON)headerJSON; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/HTTP/TUS/OCTUSHeader.m b/ownCloudSDK/HTTP/TUS/OCTUSHeader.m deleted file mode 100644 index 8ed27f2b..00000000 --- a/ownCloudSDK/HTTP/TUS/OCTUSHeader.m +++ /dev/null @@ -1,88 +0,0 @@ -// -// OCTUSHeader.m -// ownCloudSDK -// -// Created by Felix Schwarz on 28.04.20. -// Copyright © 2020 ownCloud GmbH. All rights reserved. -// - -#import "OCTUSHeader.h" - -@implementation OCTUSHeader - -- (instancetype)initWithHTTPHeaderFields:(OCHTTPHeaderFields)headerFields -{ - if ((self = [super init]) != nil) - { - [self _populateFromHTTPHeaders:headerFields]; - } - - return (self); -} - -- (void)_populateFromHTTPHeaders:(OCHTTPHeaderFields)headerFields -{ - NSString *tusVersion, *tusResumable, *tusExtensions, *tusMaxSize, *uploadOffset, *uploadLength; - - if ((tusVersion = headerFields[@"Tus-Version"]) != nil) - { - _versions = [tusVersion componentsSeparatedByString:@","]; - } - - if ((tusResumable = headerFields[@"Tus-Resumable"]) != nil) - { - _version = tusResumable; - if (_versions == nil) - { - _versions = @[ _version ]; - } - } - - if ((tusExtensions = headerFields[@"Tus-Extension"]) != nil) - { - _extensions = [tusExtensions componentsSeparatedByString:@","]; - } - - if ((tusMaxSize = headerFields[@"Tus-Max-Size"]) != nil) - { - _maximumSize = @(tusMaxSize.longLongValue); - } - - if ((uploadOffset = headerFields[@"Upload-Offset"]) != nil) - { - _uploadOffset = @(uploadOffset.longLongValue); - } - - if ((uploadLength = headerFields[@"Upload-Length"]) != nil) - { - _uploadLength = @(uploadLength.longLongValue); - } -} - -- (OCTUSSupport)supportFlags -{ - OCTUSSupport support = OCTUSSupportNone; - - if (_versions.count > 0) - { - support = OCTUSSupportAvailable; - - if ([_extensions containsObject:@"creation"]) { support |= OCTUSSupportExtensionCreation; } - if ([_extensions containsObject:@"creation-with-upload"]) { support |= OCTUSSupportExtensionCreationWithUpload; } - if ([_extensions containsObject:@"expiration"]) { support |= OCTUSSupportExtensionExpiration; } - } - - return (support); -} - -- (OCTUSInfo)info -{ - OCTUSInfo info = 0; - - OCTUSInfoSetSupport(info, self.supportFlags); - OCTUSInfoSetMaximumSize(info, self.maximumSize.unsignedLongLongValue); - - return (info); -} - -@end diff --git a/ownCloudSDK/TUS/NSString+TUSMetadata.h b/ownCloudSDK/TUS/NSString+TUSMetadata.h new file mode 100644 index 00000000..3c84ffd4 --- /dev/null +++ b/ownCloudSDK/TUS/NSString+TUSMetadata.h @@ -0,0 +1,40 @@ +// +// NSString+TUSMetadata.h +// ownCloudSDK +// +// Created by Felix Schwarz on 29.04.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import + +typedef NSString* OCTUSMetadataKey; +typedef NSString* OCTUSMetadataString; +typedef NSDictionary* OCTUSMetadata; + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (TUSMetadata) + ++ (nullable OCTUSMetadataString)stringFromTUSMetadata:(nullable OCTUSMetadata)metadata; //!< Creates an "Upload-Metadata"-styled string from an NSDictionary + +@property(nullable,strong,readonly) OCTUSMetadata tusMetadata; //!< Returns an NSDictionary from an "Upload-Metadata"-styled string + +@end + +extern NSString *OCTUSMetadataNilValue; //!< Value for keys that should be encoded solely as keys, but without value + +extern OCTUSMetadataKey OCTUSMetadataKeyFileName; +extern OCTUSMetadataKey OCTUSMetadataKeyChecksum; + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/TUS/NSString+TUSMetadata.m b/ownCloudSDK/TUS/NSString+TUSMetadata.m new file mode 100644 index 00000000..421da670 --- /dev/null +++ b/ownCloudSDK/TUS/NSString+TUSMetadata.m @@ -0,0 +1,94 @@ +// +// NSString+TUSMetadata.m +// ownCloudSDK +// +// Created by Felix Schwarz on 29.04.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "NSString+TUSMetadata.h" + +@implementation NSString (TUSMetadata) + ++ (nullable OCTUSMetadataString)stringFromTUSMetadata:(nullable OCTUSMetadata)metadata +{ + NSMutableString *mdString = nil; + + if (metadata.count > 0) + { + mdString = [NSMutableString new]; + + [metadata enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull value, BOOL * _Nonnull stop) { + if (![value isEqual:OCTUSMetadataNilValue]) + { + NSString *base64EncodedValue; + + if ((base64EncodedValue = [[value dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0]) != nil) + { + [mdString appendFormat:(mdString.length > 0) ? @",%@ %@" : @"%@ %@", key, base64EncodedValue]; + } + } + else + { + [mdString appendFormat:(mdString.length > 0) ? @",%@" : @"%@", key]; + } + }]; + } + + return (mdString); +} + +- (OCTUSMetadata)tusMetadata +{ + NSMutableDictionary *metadata = nil; + NSArray *keyValuePairs = [self componentsSeparatedByString:@","]; + + if ((keyValuePairs != nil) && (keyValuePairs.count > 0)) + { + metadata = [NSMutableDictionary new]; + + for (NSString *keyValuePair in keyValuePairs) + { + NSArray *splitPair = [keyValuePair componentsSeparatedByString:@" "]; + NSString *splitPairKey = splitPair[0]; + NSString *splitPairValue = nil; + + if (splitPair.count == 1) + { + splitPairValue = OCTUSMetadataNilValue; + } + else + { + NSString *base64EncodedValue; + + if ((base64EncodedValue = splitPair[1]) != nil) + { + NSData *utf8Value = [[NSData alloc] initWithBase64EncodedString:base64EncodedValue options:NSDataBase64DecodingIgnoreUnknownCharacters]; + splitPairValue = [[NSString alloc] initWithData:utf8Value encoding:NSUTF8StringEncoding]; + } + } + + metadata[splitPairKey] = splitPairValue; + } + } + + return (metadata); +} + +@end + +NSString *OCTUSMetadataNilValue = @""; + +OCTUSMetadataKey OCTUSMetadataKeyFileName = @"filename"; +OCTUSMetadataKey OCTUSMetadataKeyChecksum = @"checksum"; + diff --git a/ownCloudSDK/TUS/OCTUSHeader.h b/ownCloudSDK/TUS/OCTUSHeader.h new file mode 100644 index 00000000..16d6a50b --- /dev/null +++ b/ownCloudSDK/TUS/OCTUSHeader.h @@ -0,0 +1,91 @@ +// +// OCTUSHeader.h +// ownCloudSDK +// +// Created by Felix Schwarz on 28.04.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCHTTPTypes.h" +#import "NSString+TUSMetadata.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NSString* OCTUSVersion; +typedef NSString* OCTUSExtension NS_TYPED_ENUM; +typedef NSString* OCTUSHeaderName NS_TYPED_ENUM; + +typedef NS_OPTIONS(UInt8, OCTUSSupport) +{ + OCTUSSupportNone, + OCTUSSupportAvailable = (1<<0), + OCTUSSupportExtensionCreation = (1<<1), + OCTUSSupportExtensionCreationWithUpload = (1<<2), + OCTUSSupportExtensionExpiration = (1<<3) +}; + +typedef struct { + UInt8 reserved : 8; + UInt64 maximumSize : 48; + OCTUSSupport tusSupport : 8; +} _OCTUSInfoPrivate; + +typedef UInt64 OCTUSInfo; // encodes OCTUSSupport, maxSize and more (format is OCTUSInfoPrivate) + +#define OCTUSInfoGetSupport(info) ((_OCTUSInfoPrivate *)&info)->tusSupport +#define OCTUSInfoSetSupport(info,flags) ((_OCTUSInfoPrivate *)&info)->tusSupport = (flags) + +#define OCTUSInfoGetMaximumSize(info) ((_OCTUSInfoPrivate *)&info)->maximumSize +#define OCTUSInfoSetMaximumSize(info,maxSize) ((_OCTUSInfoPrivate *)&info)->maximumSize = (maxSize) + +#define OCTUSIsSupported(support,flag) ((support & flag) == flag) +#define OCTUSIsAvailable(support) OCTUSIsSupported(support,OCTUSSupportAvailable) + +@interface OCTUSHeader : NSObject + +@property(strong,nullable) OCTUSVersion version; //!< Corresponds to "Tus-Resumable" +@property(strong,nullable) NSArray *versions; //!< Corresponds to "Tus-Version" header (where available), with fallback to "Tus-Resumable" +@property(strong,nullable) NSArray *extensions; //!< Corresponds to "Tus-Extension" header + +@property(strong,nullable) NSNumber *maximumSize; //!< Corresponds to "Tus-Max-Size" header + +@property(strong,nullable) NSNumber *uploadOffset; //!< Corresponds to "Upload-Offset" header +@property(strong,nullable) NSNumber *uploadLength; //!< Corresponds to "Upload-Length" header + +@property(strong,nonatomic,nullable) OCTUSMetadata uploadMetadata; //!< Corresponds to "Upload-Metadata" (parsed) +@property(strong,nonatomic,nullable) OCTUSMetadataString uploadMetadataString; //!< Corresponds to "Upload-Metadata" (raw) + +@property(readonly,nonatomic,nullable) OCHTTPHeaderFields httpHeaderFields; + +@property(readonly,nonatomic) OCTUSSupport supportFlags; //!< Returns TUS support info compressed as set of flags +@property(readonly,nonatomic) OCTUSInfo info; //!< Returns TUS info compressed to an integer + +- (instancetype)initWithHTTPHeaderFields:(OCHTTPStaticHeaderFields)headerFields; +- (instancetype)initWithTUSInfo:(OCTUSInfo)info; + +@end + +extern const OCTUSHeaderName OCTUSHeaderNameTusVersion; +extern const OCTUSHeaderName OCTUSHeaderNameTusResumable; +extern const OCTUSHeaderName OCTUSHeaderNameTusExtension; +extern const OCTUSHeaderName OCTUSHeaderNameTusMaxSize; +extern const OCTUSHeaderName OCTUSHeaderNameUploadOffset; +extern const OCTUSHeaderName OCTUSHeaderNameUploadLength; +extern const OCTUSHeaderName OCTUSHeaderNameUploadMetadata; + +extern const OCTUSExtension OCTUSExtensionCreation; +extern const OCTUSExtension OCTUSExtensionCreationWithUpload; +extern const OCTUSExtension OCTUSExtensionExpiration; + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/TUS/OCTUSHeader.m b/ownCloudSDK/TUS/OCTUSHeader.m new file mode 100644 index 00000000..dba3eeba --- /dev/null +++ b/ownCloudSDK/TUS/OCTUSHeader.m @@ -0,0 +1,280 @@ +// +// OCTUSHeader.m +// ownCloudSDK +// +// Created by Felix Schwarz on 28.04.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCTUSHeader.h" + +@interface OCTUSHeader () +{ + OCTUSMetadata _uploadMetadata; + OCTUSMetadataString _uploadMetadataString; +} +@end + +@implementation OCTUSHeader + +@dynamic httpHeaderFields; + +@dynamic supportFlags; +@dynamic info; + +- (instancetype)initWithHTTPHeaderFields:(OCHTTPStaticHeaderFields)headerFields +{ + if ((self = [super init]) != nil) + { + [self _populateFromHTTPHeaders:headerFields]; + } + + return (self); +} + +- (instancetype)initWithTUSInfo:(OCTUSInfo)info +{ + if ((self = [super init]) != nil) + { + [self _populateFromTusSupport:OCTUSInfoGetSupport(info)]; + + UInt64 maxSize = OCTUSInfoGetMaximumSize(info); + + if (maxSize != 0) + { + _maximumSize = @(maxSize); + } + } + + return (self); +} + +- (void)_populateFromTusSupport:(OCTUSSupport)support +{ + NSString *extensions[5]; + NSUInteger extensionCount = 0; + + #define ExpandFlag(flag,extension) \ + if (OCTUSIsSupported(support, flag)) \ + { \ + extensions[extensionCount] = extension; \ + extensionCount++; \ + } + + ExpandFlag(OCTUSSupportExtensionCreation, OCTUSExtensionCreation) + ExpandFlag(OCTUSSupportExtensionCreationWithUpload, OCTUSExtensionCreationWithUpload) + ExpandFlag(OCTUSSupportExtensionExpiration, OCTUSExtensionExpiration) + + if (extensionCount > 0) + { + _extensions = [[NSArray alloc] initWithObjects:extensions count:extensionCount]; + } +} + +- (void)_populateFromHTTPHeaders:(OCHTTPStaticHeaderFields)headerFields +{ + NSString *tusVersion, *tusResumable, *tusExtensions, *tusMaxSize, *uploadOffset, *uploadLength; + + if ((tusVersion = headerFields[OCTUSHeaderNameTusVersion]) != nil) + { + _versions = [tusVersion componentsSeparatedByString:@","]; + } + + if ((tusResumable = headerFields[OCTUSHeaderNameTusResumable]) != nil) + { + _version = tusResumable; + if (_versions == nil) + { + _versions = @[ _version ]; + } + } + + if ((tusExtensions = headerFields[OCTUSHeaderNameTusExtension]) != nil) + { + _extensions = [tusExtensions componentsSeparatedByString:@","]; + } + + if ((tusMaxSize = headerFields[OCTUSHeaderNameTusMaxSize]) != nil) + { + _maximumSize = @(tusMaxSize.longLongValue); + } + + if ((uploadOffset = headerFields[OCTUSHeaderNameUploadOffset]) != nil) + { + _uploadOffset = @(uploadOffset.longLongValue); + } + + if ((uploadLength = headerFields[OCTUSHeaderNameUploadLength]) != nil) + { + _uploadLength = @(uploadLength.longLongValue); + } +} + +- (OCTUSMetadata)uploadMetadata +{ + if ((_uploadMetadata == nil) && (_uploadMetadataString != nil)) + { + _uploadMetadata = _uploadMetadataString.tusMetadata; + } + + return (_uploadMetadata); +} + +- (void)setUploadMetadata:(OCTUSMetadata)uploadMetaData +{ + _uploadMetadataString = nil; + _uploadMetadata = uploadMetaData; +} + +- (OCTUSMetadataString)uploadMetadataString +{ + if ((_uploadMetadataString == nil) && (_uploadMetadata != nil)) + { + _uploadMetadataString = [NSString stringFromTUSMetadata:_uploadMetadata]; + } + + return (_uploadMetadataString); +} + +- (void)setUploadMetadataString:(OCTUSMetadataString)uploadMetaDataString +{ + _uploadMetadata = nil; + _uploadMetadataString = uploadMetaDataString; +} + +- (OCHTTPHeaderFields)httpHeaderFields +{ + OCHTTPHeaderFields headerFields = [NSMutableDictionary new]; + + if (_version != nil) + { + headerFields[OCTUSHeaderNameTusResumable] = _version; + } + + if (_versions != nil) + { + headerFields[OCTUSHeaderNameTusVersion] = [_versions componentsJoinedByString:@","]; + } +// else if (_version != nil) +// { +// headerFields[OCTUSHeaderNameTusVersion] = _version; +// } + + if (_extensions != nil) + { + headerFields[OCTUSHeaderNameTusExtension] = [_extensions componentsJoinedByString:@","]; + } + + if (_maximumSize != nil) + { + headerFields[OCTUSHeaderNameTusMaxSize] = _maximumSize.stringValue; + } + + if (_uploadOffset != nil) + { + headerFields[OCTUSHeaderNameUploadOffset] = _uploadOffset.stringValue; + } + + if (_uploadLength != nil) + { + headerFields[OCTUSHeaderNameUploadLength] = _uploadLength.stringValue; + } + + if ((_uploadMetadata!=nil) || (_uploadMetadataString != nil)) + { + headerFields[OCTUSHeaderNameUploadMetadata] = self.uploadMetadataString; + } + + return ((headerFields.count > 0) ? headerFields : nil); +} + +- (OCTUSSupport)supportFlags +{ + OCTUSSupport support = OCTUSSupportNone; + + if ((_versions.count > 0) || (_extensions.count > 0)) + { + support = OCTUSSupportAvailable; + + if ([_extensions containsObject:OCTUSExtensionCreation]) { support |= OCTUSSupportExtensionCreation; } + if ([_extensions containsObject:OCTUSExtensionCreationWithUpload]) { support |= OCTUSSupportExtensionCreationWithUpload; } + if ([_extensions containsObject:OCTUSExtensionExpiration]) { support |= OCTUSSupportExtensionExpiration; } + } + + return (support); +} + +- (OCTUSInfo)info +{ + OCTUSInfo info = 0; + + OCTUSInfoSetSupport(info, self.supportFlags); + OCTUSInfoSetMaximumSize(info, self.maximumSize.unsignedLongLongValue); + + return (info); +} + +#pragma mark - NSSecureCoding ++ (BOOL)supportsSecureCoding +{ + return (YES); +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + if ((self = [super init]) != nil) + { + NSSet *allowedClasses = [[NSSet alloc] initWithObjects:NSString.class, NSArray.class, nil]; + + _version = [coder decodeObjectOfClass:NSString.class forKey:@"version"]; + + _versions = [coder decodeObjectOfClasses:allowedClasses forKey:@"versions"]; + _extensions = [coder decodeObjectOfClasses:allowedClasses forKey:@"extensions"]; + + _maximumSize = [coder decodeObjectOfClass:NSNumber.class forKey:@"maximumSize"]; + _uploadOffset = [coder decodeObjectOfClass:NSNumber.class forKey:@"uploadOffset"]; + _uploadLength = [coder decodeObjectOfClass:NSNumber.class forKey:@"uploadLength"]; + + _uploadMetadataString = [coder decodeObjectOfClass:NSString.class forKey:@"uploadMetadata"]; + } + + return (self); +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:_version forKey:@"version"]; + + [coder encodeObject:_versions forKey:@"versions"]; + [coder encodeObject:_extensions forKey:@"extensions"]; + + [coder encodeObject:_maximumSize forKey:@"maximumSize"]; + [coder encodeObject:_uploadOffset forKey:@"uploadOffset"]; + [coder encodeObject:_uploadLength forKey:@"uploadLength"]; + + [coder encodeObject:self.uploadMetadataString forKey:@"uploadMetadata"]; +} + +@end + +const OCTUSHeaderName OCTUSHeaderNameTusVersion = @"Tus-Version"; +const OCTUSHeaderName OCTUSHeaderNameTusResumable = @"Tus-Resumable"; +const OCTUSHeaderName OCTUSHeaderNameTusExtension = @"Tus-Extension"; +const OCTUSHeaderName OCTUSHeaderNameTusMaxSize = @"Tus-Max-Size"; +const OCTUSHeaderName OCTUSHeaderNameUploadOffset = @"Upload-Offset"; +const OCTUSHeaderName OCTUSHeaderNameUploadLength = @"Upload-Length"; +const OCTUSHeaderName OCTUSHeaderNameUploadMetadata = @"Upload-Metadata"; + +const OCTUSExtension OCTUSExtensionCreation = @"creation"; +const OCTUSExtension OCTUSExtensionCreationWithUpload = @"creation-with-upload"; +const OCTUSExtension OCTUSExtensionExpiration = @"expiration"; diff --git a/ownCloudSDK/TUS/OCTUSJob.h b/ownCloudSDK/TUS/OCTUSJob.h new file mode 100644 index 00000000..66de041d --- /dev/null +++ b/ownCloudSDK/TUS/OCTUSJob.h @@ -0,0 +1,71 @@ +// +// OCTUSJob.h +// ownCloudSDK +// +// Created by Felix Schwarz on 29.04.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCTUSHeader.h" +#import "OCChecksum.h" +#import "OCEventTarget.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OCTUSJobSegment; + +@interface OCTUSJob : NSObject + +@property(strong) OCTUSHeader *header; + +@property(strong) NSURL *fileURL; +@property(strong) NSURL *segmentFolderURL; + +@property(strong,nullable) NSNumber *uploadOffset; + +@property(assign) NSUInteger maxSegmentSize; + +@property(strong,nullable) NSURL *creationURL; //!< URL to direct creation requests to +@property(strong,nullable) NSURL *uploadURL; //!< URL to direct upload requests to + +@property(strong,nullable) NSString *futureItemPath; //!< Future path of the item on the server (after upload) + +@property(strong,nullable) NSString *fileName; +@property(strong,nullable) NSNumber *fileSize; +@property(strong,nullable) NSDate *fileModDate; +@property(strong,nullable) OCChecksum *fileChecksum; + +@property(strong,nullable) OCEventTarget *eventTarget; + +- (instancetype)initWithHeader:(OCTUSHeader *)header segmentFolderURL:(NSURL *)segmentFolder fileURL:(NSURL *)fileURL creationURL:(NSURL *)creationURL; + +- (nullable OCTUSJobSegment *)requestSegmentFromOffset:(NSUInteger)offset withSize:(NSUInteger)size error:(NSError * _Nullable * _Nullable)outError; + +- (void)destroy; //!< Erase the .segmentFolder + +@end + + +@interface OCTUSJobSegment : NSObject + +@property(assign) NSUInteger offset; +@property(assign) NSUInteger size; +@property(strong,nullable) NSURL *url; + +@property(readonly,nonatomic) BOOL isValid; + +@end + + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/TUS/OCTUSJob.m b/ownCloudSDK/TUS/OCTUSJob.m new file mode 100644 index 00000000..4f931fb4 --- /dev/null +++ b/ownCloudSDK/TUS/OCTUSJob.m @@ -0,0 +1,337 @@ +// +// OCTUSJob.m +// ownCloudSDK +// +// Created by Felix Schwarz on 29.04.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCTUSJob.h" +#import "OCChecksum.h" +#import "OCLogger.h" + +@interface OCTUSJob () +{ + OCTUSJobSegment *_lastSegment; +} +@end + +@implementation OCTUSJob + +- (instancetype)initWithHeader:(OCTUSHeader *)header segmentFolderURL:(NSURL *)segmentFolder fileURL:(NSURL *)fileURL creationURL:(NSURL *)creationURL +{ + if ((self = [super init]) != nil) + { + _header = header; + _creationURL = creationURL; + + _maxSegmentSize = _header.maximumSize.unsignedIntegerValue; + + _segmentFolderURL = segmentFolder; + _fileURL = fileURL; + } + + return (self); +} + +#pragma mark - Segmentation +- (nullable OCTUSJobSegment *)requestSegmentFromOffset:(NSUInteger)offset withSize:(NSUInteger)size error:(NSError * _Nullable * _Nullable)outError +{ + if ((_lastSegment != nil) && (_lastSegment.offset == offset) && (_lastSegment.size == size) && _lastSegment.isValid) + { + return (_lastSegment); + } + else + { + if (_lastSegment.isValid) + { + [NSFileManager.defaultManager removeItemAtURL:_lastSegment.url error:NULL]; + _lastSegment = nil; + } + + if (_fileURL != nil) + { + NSError *error = nil; + NSURL *segmentFileURL = [_segmentFolderURL URLByAppendingPathComponent:NSUUID.UUID.UUIDString]; + NSFileHandle *srcFile = nil; + NSFileHandle *dstFile = nil; + NSUInteger copyChunkSize = 100000; + NSUInteger bytesCopied = 0; + BOOL copySuccessful = NO; + + do + { + if ((srcFile = [NSFileHandle fileHandleForReadingFromURL:_fileURL error:&error]) == nil) + { + OCLogError(@"Error opening file %@ for reading: %@", _fileURL, error); + break; + } + + if (![NSFileManager.defaultManager createFileAtPath:segmentFileURL.path contents:nil attributes:nil]) + { + OCLogError(@"Error creating segment file %@", segmentFileURL); + break; + } + + if ((dstFile = [NSFileHandle fileHandleForWritingToURL:segmentFileURL error:&error]) == nil) + { + OCLogError(@"Error opening segment file %@ for writing: %@", segmentFileURL, error); + break; + } + + if (@available(iOS 13, *)) + { + if (![srcFile seekToOffset:offset error:&error]) + { + OCLogError(@"Error seeking to position %lu in file %@: %@", (unsigned long)offset, _fileURL, error); + break; + } + + while ((bytesCopied < size) && (copyChunkSize > 0)) { + @autoreleasepool { + NSData *data = nil; + + if (copyChunkSize > (size - bytesCopied)) + { + copyChunkSize = (size - bytesCopied); + } + + data = [srcFile readDataUpToLength:copyChunkSize error:&error]; + + if (error != nil) + { + OCLogError(@"Error reading %lu bytes from %lu in file %@: %@", (unsigned long)size, (unsigned long)offset, _fileURL, error); + break; + } + + if (data != nil) + { + if (![dstFile writeData:data error:&error]) + { + OCLogError(@"Error writiung %lu bytes to file %@: %@", (unsigned long)data.length, _fileURL, error); + break; + } + } + + bytesCopied += data.length; + + if ((data.length == 0) || (error != nil)) { break; } + } + }; + + if (error != nil) { break; } + } + else + { + @try + { + [srcFile seekToFileOffset:offset]; + + while ((bytesCopied < size) && (copyChunkSize > 0)) { + @autoreleasepool { + NSData *data = nil; + + if (copyChunkSize > (size - bytesCopied)) + { + copyChunkSize = (size - bytesCopied); + } + + if ((data = [srcFile readDataOfLength:copyChunkSize]) != nil) + { + [dstFile writeData:data]; + + bytesCopied += data.length; + } + + if ((data.length == 0) || (error != nil)) { break; } + } + }; + } + @catch(NSException *exception) + { + OCLogError(@"Exception copying %lu bytes from %@ to %@: %@", (unsigned long)size, _fileURL, segmentFileURL, OCLogPrivate(exception)); + break; + } + } + + copySuccessful = YES; + }while(false); + + if (@available(iOS 13, *)) + { + [srcFile closeAndReturnError:&error]; + [dstFile closeAndReturnError:&error]; + } + else + { + @try + { + [srcFile closeFile]; + [dstFile closeFile]; + } + @catch(NSException *exception) + { + OCLogError(@"Exception closing %@ + %@: %@", _fileURL, segmentFileURL, OCLogPrivate(exception)); + } + } + + srcFile = nil; + dstFile = nil; + + if (copySuccessful) + { + OCTUSJobSegment *segment = [OCTUSJobSegment new]; + + segment.offset = offset; + segment.size = bytesCopied; + + segment.url = segmentFileURL; + + _lastSegment = segment; + + return (segment); + } + else + { + if (outError != NULL) + { + *outError = error; + } + } + } + } + + return (nil); +} + +#pragma mark - Destroy +- (void)destroy +{ + if (_segmentFolderURL != nil) + { + NSError *error = nil; + + NSFileManager *fileManager = [NSFileManager new]; + fileManager.delegate = self; + + if (![NSFileManager.defaultManager removeItemAtURL:_segmentFolderURL error:&error]) + { + OCLogError(@"Error destroying OCTUSJob segmentFolder at %@: %@", _segmentFolderURL, error); + } + + _segmentFolderURL = nil; + } +} + +- (BOOL)fileManager:(NSFileManager *)fileManager shouldRemoveItemAtURL:(NSURL *)URL +{ + return (YES); +} + +#pragma mark - NSSecureCoding ++ (BOOL)supportsSecureCoding +{ + return (YES); +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + if ((self = [super init]) != nil) + { + _header = [coder decodeObjectOfClass:OCTUSHeader.class forKey:@"header"]; + + _fileURL = [coder decodeObjectOfClass:NSURL.class forKey:@"fileURL"]; + _segmentFolderURL = [coder decodeObjectOfClass:NSURL.class forKey:@"segmentFolderURL"]; + + _uploadOffset = [coder decodeObjectOfClass:NSNumber.class forKey:@"uploadOffset"]; + + _maxSegmentSize = [coder decodeInt64ForKey:@"maxSegmentSize"]; + + _creationURL = [coder decodeObjectOfClass:NSURL.class forKey:@"creationURL"]; + _uploadURL = [coder decodeObjectOfClass:NSURL.class forKey:@"uploadURL"]; + + _futureItemPath = [coder decodeObjectOfClass:NSString.class forKey:@"futureItemPath"]; + + _fileName = [coder decodeObjectOfClass:NSString.class forKey:@"fileName"]; + _fileSize = [coder decodeObjectOfClass:NSNumber.class forKey:@"fileSize"]; + _fileModDate = [coder decodeObjectOfClass:NSDate.class forKey:@"fileModDate"]; + _fileChecksum = [coder decodeObjectOfClass:OCChecksum.class forKey:@"fileChecksum"]; + + _eventTarget = [coder decodeObjectOfClass:OCEventTarget.class forKey:@"eventTarget"]; + + _lastSegment = [coder decodeObjectOfClass:OCTUSJobSegment.class forKey:@"lastSegment"]; + } + + return (self); +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:_header forKey:@"header"]; + + [coder encodeObject:_fileURL forKey:@"fileURL"]; + [coder encodeObject:_segmentFolderURL forKey:@"segmentFolderURL"]; + + [coder encodeObject:_uploadOffset forKey:@"uploadOffset"]; + + [coder encodeInt64:_maxSegmentSize forKey:@"maxSegmentSize"]; + + [coder encodeObject:_creationURL forKey:@"creationURL"]; + [coder encodeObject:_uploadURL forKey:@"uploadURL"]; + + [coder encodeObject:_futureItemPath forKey:@"futureItemPath"]; + + [coder encodeObject:_fileName forKey:@"fileName"]; + [coder encodeObject:_fileSize forKey:@"fileSize"]; + [coder encodeObject:_fileModDate forKey:@"fileModDate"]; + [coder encodeObject:_fileChecksum forKey:@"fileChecksum"]; + + [coder encodeObject:_eventTarget forKey:@"eventTarget"]; + + [coder encodeObject:_lastSegment forKey:@"lastSegment"]; +} + +@end + +@implementation OCTUSJobSegment + ++ (BOOL)supportsSecureCoding +{ + return (YES); +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + if ((self = [super init]) != nil) + { + _url = [coder decodeObjectOfClass:NSURL.class forKey:@"url"]; + _offset = [coder decodeInt64ForKey:@"offset"]; + _size = [coder decodeInt64ForKey:@"size"]; + } + + return (self); +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeObject:_url forKey:@"url"]; + [coder encodeInt64:_offset forKey:@"offset"]; + [coder encodeInt64:_size forKey:@"size"]; +} + +- (BOOL)isValid +{ + return ((_url != nil) && (_size > 0) && [NSFileManager.defaultManager fileExistsAtPath:_url.path]); +} + +@end diff --git a/ownCloudSDK/ownCloudSDK.h b/ownCloudSDK/ownCloudSDK.h index c5f6d515..e69eb850 100644 --- a/ownCloudSDK/ownCloudSDK.h +++ b/ownCloudSDK/ownCloudSDK.h @@ -140,6 +140,7 @@ FOUNDATION_EXPORT const unsigned char ownCloudSDKVersionString[]; #import #import +#import #import #import From 06a41e903ba7c3e49ce2c43ddfe252deaadd3934 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Wed, 27 May 2020 16:25:49 +0200 Subject: [PATCH 3/3] - OCCapabilities - add support for TUS capabilities information provided via the capabilities endpoint - OCConnection - apply OCCapabilities.tusMaxChunkSize to TUS uploads if available - OCTUSHeader - clarify .maximumSize is the maximum file size and add new .maximumChunkSize to supply a maximum chunk size - add new OCTUSCapabilityKey and OCTUSCapability types as needed by OCCapabilities --- .../Connection/Capabilities/OCCapabilities.h | 12 +++ .../Connection/Capabilities/OCCapabilities.m | 88 +++++++++++++++++++ ownCloudSDK/Connection/OCConnection.m | 10 ++- ownCloudSDK/TUS/OCTUSHeader.h | 6 +- ownCloudSDK/TUS/OCTUSHeader.m | 2 + ownCloudSDK/TUS/OCTUSJob.m | 2 +- 6 files changed, 117 insertions(+), 3 deletions(-) diff --git a/ownCloudSDK/Connection/Capabilities/OCCapabilities.h b/ownCloudSDK/Connection/Capabilities/OCCapabilities.h index 82492b74..85471b6d 100644 --- a/ownCloudSDK/Connection/Capabilities/OCCapabilities.h +++ b/ownCloudSDK/Connection/Capabilities/OCCapabilities.h @@ -19,6 +19,7 @@ #import #import "OCChecksumAlgorithm.h" #import "OCShare.h" +#import "OCTUSHeader.h" NS_ASSUME_NONNULL_BEGIN @@ -55,6 +56,17 @@ typedef NSNumber* OCCapabilityBool; @property(readonly,nullable,nonatomic) NSString *davChunkingVersion; @property(readonly,nullable,nonatomic) NSArray *davReports; +#pragma mark - TUS +@property(readonly,nonatomic) BOOL tusSupported; +@property(readonly,nullable,nonatomic) OCTUSCapabilities tusCapabilities; +@property(readonly,nullable,nonatomic) NSArray *tusVersions; +@property(readonly,nullable,nonatomic) OCTUSVersion tusResumable; +@property(readonly,nullable,nonatomic) NSArray *tusExtensions; +@property(readonly,nullable,nonatomic) NSNumber *tusMaxChunkSize; +@property(readonly,nullable,nonatomic) OCHTTPMethod tusHTTPMethodOverride; + +@property(readonly,nullable,nonatomic) OCTUSHeader *tusCapabilitiesHeader; //!< .tusCapabilities translated into an OCTUSHeader + #pragma mark - Files @property(readonly,nullable,nonatomic) OCCapabilityBool supportsPrivateLinks; @property(readonly,nullable,nonatomic) OCCapabilityBool supportsBigFileChunking; diff --git a/ownCloudSDK/Connection/Capabilities/OCCapabilities.m b/ownCloudSDK/Connection/Capabilities/OCCapabilities.m index 873d5e95..1a379703 100644 --- a/ownCloudSDK/Connection/Capabilities/OCCapabilities.m +++ b/ownCloudSDK/Connection/Capabilities/OCCapabilities.m @@ -25,6 +25,10 @@ @interface OCCapabilities() { NSDictionary *_capabilities; + + OCTUSHeader *_tusCapabilitiesHeader; + NSArray *_tusVersions; + NSArray *_tusExtensions; } @end @@ -57,6 +61,17 @@ @implementation OCCapabilities @dynamic davChunkingVersion; @dynamic davReports; +#pragma mark - TUS +@dynamic tusSupported; +@dynamic tusCapabilities; +@dynamic tusVersions; +@dynamic tusResumable; +@dynamic tusExtensions; +@dynamic tusMaxChunkSize; +@dynamic tusHTTPMethodOverride; + +@dynamic tusCapabilitiesHeader; + #pragma mark - Files @dynamic supportsPrivateLinks; @dynamic supportsBigFileChunking; @@ -229,6 +244,79 @@ - (NSString *)davChunkingVersion return (OCTypedCast(_capabilities[@"dav"][@"reports"], NSArray)); } +#pragma mark - TUS +- (BOOL)tusSupported +{ + return (self.tusResumable.length > 0); +} + +- (OCTUSCapabilities)tusCapabilities +{ + return (OCTypedCast(_capabilities[@"files"][@"tus_support"], NSDictionary)); +} + +- (NSArray *)tusVersions +{ + if (_tusVersions) + { + _tusVersions = [OCTypedCast(self.tusCapabilities[@"version"], NSString) componentsSeparatedByString:@","]; + } + + return (_tusVersions); +} + +- (OCTUSVersion)tusResumable +{ + return(OCTypedCast(self.tusCapabilities[@"resumable"], NSString)); +} + +- (NSArray *)tusExtensions +{ + if (_tusExtensions == nil) + { + NSString *tusExtensionsString = OCTypedCast(self.tusCapabilities[@"extension"], NSString); + + _tusExtensions = [tusExtensionsString componentsSeparatedByString:@","]; + } + + return (_tusExtensions); +} + +- (NSNumber *)tusMaxChunkSize +{ + return(OCTypedCast(self.tusCapabilities[@"max_chunk_size"], NSNumber)); +} + +- (OCHTTPMethod)tusHTTPMethodOverride +{ + NSString *httpMethodOverride = OCTypedCast(self.tusCapabilities[@"http_method_override"], NSString); + + if (httpMethodOverride.length == 0) + { + return (nil); + } + + return(httpMethodOverride); +} + +- (OCTUSHeader *)tusCapabilitiesHeader +{ + if ((_tusCapabilitiesHeader == nil) && self.tusSupported) + { + OCTUSHeader *header = [[OCTUSHeader alloc] init]; + + header.extensions = self.tusExtensions; + header.version = self.tusResumable; + header.versions = self.tusVersions; + + header.maximumChunkSize = self.tusMaxChunkSize; + + _tusCapabilitiesHeader = header; + } + + return (_tusCapabilitiesHeader); +} + #pragma mark - Files - (OCCapabilityBool)supportsPrivateLinks { diff --git a/ownCloudSDK/Connection/OCConnection.m b/ownCloudSDK/Connection/OCConnection.m index fdbebe1e..e359b087 100644 --- a/ownCloudSDK/Connection/OCConnection.m +++ b/ownCloudSDK/Connection/OCConnection.m @@ -1528,7 +1528,15 @@ - (OCProgress *)_tusUploadFileFromURL:(NSURL *)sourceURL withName:(NSString *)fi tusJob.eventTarget = eventTarget; - tusJob.maxSegmentSize = 111000; // TODO: here for testing only, remove + if (tusJob.maxSegmentSize == 0) + { + NSNumber *capabilitiesTusMaxChunkSize; + + if ((capabilitiesTusMaxChunkSize = self.capabilities.tusMaxChunkSize) != nil) + { + tusJob.maxSegmentSize = capabilitiesTusMaxChunkSize.unsignedIntegerValue; + } + } tusProgress = [self _continueTusJob:tusJob lastTask:nil]; } diff --git a/ownCloudSDK/TUS/OCTUSHeader.h b/ownCloudSDK/TUS/OCTUSHeader.h index 16d6a50b..ebdd4095 100644 --- a/ownCloudSDK/TUS/OCTUSHeader.h +++ b/ownCloudSDK/TUS/OCTUSHeader.h @@ -26,6 +26,9 @@ typedef NSString* OCTUSVersion; typedef NSString* OCTUSExtension NS_TYPED_ENUM; typedef NSString* OCTUSHeaderName NS_TYPED_ENUM; +typedef NSString* OCTUSCapabilityKey NS_TYPED_ENUM; +typedef NSDictionary* OCTUSCapabilities; + typedef NS_OPTIONS(UInt8, OCTUSSupport) { OCTUSSupportNone, @@ -58,7 +61,8 @@ typedef UInt64 OCTUSInfo; // encodes OCTUSSupport, maxSize and more (format is O @property(strong,nullable) NSArray *versions; //!< Corresponds to "Tus-Version" header (where available), with fallback to "Tus-Resumable" @property(strong,nullable) NSArray *extensions; //!< Corresponds to "Tus-Extension" header -@property(strong,nullable) NSNumber *maximumSize; //!< Corresponds to "Tus-Max-Size" header +@property(strong,nullable) NSNumber *maximumSize; //!< Corresponds to "Tus-Max-Size" header (maximum size of entire upload) +@property(strong,nullable) NSNumber *maximumChunkSize; //!< Maximum chunk size to apply @property(strong,nullable) NSNumber *uploadOffset; //!< Corresponds to "Upload-Offset" header @property(strong,nullable) NSNumber *uploadLength; //!< Corresponds to "Upload-Length" header diff --git a/ownCloudSDK/TUS/OCTUSHeader.m b/ownCloudSDK/TUS/OCTUSHeader.m index dba3eeba..d8ba625f 100644 --- a/ownCloudSDK/TUS/OCTUSHeader.m +++ b/ownCloudSDK/TUS/OCTUSHeader.m @@ -242,6 +242,7 @@ - (instancetype)initWithCoder:(NSCoder *)coder _extensions = [coder decodeObjectOfClasses:allowedClasses forKey:@"extensions"]; _maximumSize = [coder decodeObjectOfClass:NSNumber.class forKey:@"maximumSize"]; + _maximumChunkSize = [coder decodeObjectOfClass:NSNumber.class forKey:@"maximumChunkSize"]; _uploadOffset = [coder decodeObjectOfClass:NSNumber.class forKey:@"uploadOffset"]; _uploadLength = [coder decodeObjectOfClass:NSNumber.class forKey:@"uploadLength"]; @@ -259,6 +260,7 @@ - (void)encodeWithCoder:(NSCoder *)coder [coder encodeObject:_extensions forKey:@"extensions"]; [coder encodeObject:_maximumSize forKey:@"maximumSize"]; + [coder encodeObject:_maximumChunkSize forKey:@"maximumChunkSize"]; [coder encodeObject:_uploadOffset forKey:@"uploadOffset"]; [coder encodeObject:_uploadLength forKey:@"uploadLength"]; diff --git a/ownCloudSDK/TUS/OCTUSJob.m b/ownCloudSDK/TUS/OCTUSJob.m index 4f931fb4..f0a64d76 100644 --- a/ownCloudSDK/TUS/OCTUSJob.m +++ b/ownCloudSDK/TUS/OCTUSJob.m @@ -35,7 +35,7 @@ - (instancetype)initWithHeader:(OCTUSHeader *)header segmentFolderURL:(NSURL *)s _header = header; _creationURL = creationURL; - _maxSegmentSize = _header.maximumSize.unsignedIntegerValue; + _maxSegmentSize = _header.maximumChunkSize.unsignedIntegerValue; _segmentFolderURL = segmentFolder; _fileURL = fileURL;