diff --git a/ownCloudSDK.xcodeproj/project.pbxproj b/ownCloudSDK.xcodeproj/project.pbxproj index c06d2eeb..a2aeb60b 100644 --- a/ownCloudSDK.xcodeproj/project.pbxproj +++ b/ownCloudSDK.xcodeproj/project.pbxproj @@ -99,8 +99,6 @@ DC2D646821C3D71000EB26FD /* OCCore+Thumbnails.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2D646721C3D70F00EB26FD /* OCCore+Thumbnails.h */; }; DC2D649C21C7B3DF00EB26FD /* OCAuthenticationMethodOAuth2+OCMocking.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2D649A21C7B3DF00EB26FD /* OCAuthenticationMethodOAuth2+OCMocking.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC2D649D21C7B3DF00EB26FD /* OCAuthenticationMethodOAuth2+OCMocking.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2D649B21C7B3DF00EB26FD /* OCAuthenticationMethodOAuth2+OCMocking.m */; }; - DC2D701821CA633100EB26FD /* OCSyncRecordStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2D701621CA633000EB26FD /* OCSyncRecordStatus.h */; }; - DC2D701921CA633100EB26FD /* OCSyncRecordStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2D701721CA633100EB26FD /* OCSyncRecordStatus.m */; }; DC2EF1B720ECBB7100BA2A3E /* OCKeyValueStore.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2EF1B520ECBB7100BA2A3E /* OCKeyValueStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC2EF1B820ECBB7100BA2A3E /* OCKeyValueStore.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2EF1B620ECBB7100BA2A3E /* OCKeyValueStore.m */; }; DC2EF1C020ED782400BA2A3E /* OCConnectionQueue+BackgroundSessionRecovery.h in Headers */ = {isa = PBXBuildFile; fileRef = DC2EF1BE20ED782400BA2A3E /* OCConnectionQueue+BackgroundSessionRecovery.h */; }; @@ -167,7 +165,6 @@ DC594B1F21EFD7F900B882C4 /* BookmarkManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DC594B1E21EFD7F900B882C4 /* BookmarkManagerTests.m */; }; DC594B2021EFDF8700B882C4 /* OCConnection+Jobs.m in Sources */ = {isa = PBXBuildFile; fileRef = DC24F8E321E2411E00C9119C /* OCConnection+Jobs.m */; }; DC5A20312074E8890083DB7D /* CoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DC5A20302074E8890083DB7D /* CoreTests.m */; }; - DC5A203420750BEE0083DB7D /* OCCore+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = DC5A203320750BEE0083DB7D /* OCCore+Internal.h */; }; DC5A794F21E5FAF20045BCAA /* OCConnection+Signals.m in Sources */ = {isa = PBXBuildFile; fileRef = DC5A794D21E5FAF20045BCAA /* OCConnection+Signals.m */; }; DC62FD62211B11FD0034B628 /* OCItem+OCThumbnail.h in Headers */ = {isa = PBXBuildFile; fileRef = DC62FD60211B11FD0034B628 /* OCItem+OCThumbnail.h */; }; DC62FD63211B11FD0034B628 /* OCItem+OCThumbnail.m in Sources */ = {isa = PBXBuildFile; fileRef = DC62FD61211B11FD0034B628 /* OCItem+OCThumbnail.m */; }; @@ -220,6 +217,9 @@ DC7E0A89203732B3006111FA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DC7E0A88203732B3006111FA /* main.m */; }; DC7E0A8F20373381006111FA /* BookmarkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DC7E0A712036F60C006111FA /* BookmarkTests.m */; }; DC812558210B548F00A9C037 /* OCTestTarget.m in Sources */ = {isa = PBXBuildFile; fileRef = DC812557210B548F00A9C037 /* OCTestTarget.m */; }; + DC8245B321FB31E500775AB9 /* OCActivityManager.h in Headers */ = {isa = PBXBuildFile; fileRef = DC8245B121FB31E500775AB9 /* OCActivityManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC8245B421FB31E500775AB9 /* OCActivityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DC8245B221FB31E500775AB9 /* OCActivityManager.m */; }; + DC8245BA21FB334200775AB9 /* OCCore+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = DC8245B921FB334200775AB9 /* OCCore+Internal.h */; }; DC8556ED204DEA2900189B9A /* OCConnectionDAVRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = DC8556EB204DEA2900189B9A /* OCConnectionDAVRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC8556EE204DEA2900189B9A /* OCConnectionDAVRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = DC8556EC204DEA2900189B9A /* OCConnectionDAVRequest.m */; }; DC8556F1204DEB9200189B9A /* OCXMLNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DC8556EF204DEB9200189B9A /* OCXMLNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -269,6 +269,12 @@ DCADC04E2072D54200DB8E83 /* OCSQLiteTableSchema.m in Sources */ = {isa = PBXBuildFile; fileRef = DCADC04C2072D54200DB8E83 /* OCSQLiteTableSchema.m */; }; DCADC0522072DE6600DB8E83 /* OCSQLiteMigration.h in Headers */ = {isa = PBXBuildFile; fileRef = DCADC0502072DE6600DB8E83 /* OCSQLiteMigration.h */; }; DCADC0532072DE6600DB8E83 /* OCSQLiteMigration.m in Sources */ = {isa = PBXBuildFile; fileRef = DCADC0512072DE6600DB8E83 /* OCSQLiteMigration.m */; }; + DCAEB06921FA617D0067E147 /* OCActivity.h in Headers */ = {isa = PBXBuildFile; fileRef = DCAEB06721FA617D0067E147 /* OCActivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCAEB06A21FA617D0067E147 /* OCActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = DCAEB06821FA617D0067E147 /* OCActivity.m */; }; + DCAEB06D21FA63D80067E147 /* OCSyncRecordActivity.h in Headers */ = {isa = PBXBuildFile; fileRef = DCAEB06B21FA63D80067E147 /* OCSyncRecordActivity.h */; }; + DCAEB06E21FA63D80067E147 /* OCSyncRecordActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = DCAEB06C21FA63D80067E147 /* OCSyncRecordActivity.m */; }; + DCAEB07121FA67060067E147 /* OCActivityUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = DCAEB06F21FA67060067E147 /* OCActivityUpdate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCAEB07221FA67060067E147 /* OCActivityUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = DCAEB07021FA67060067E147 /* OCActivityUpdate.m */; }; DCB0A45921B7F76800FAC4E9 /* NSError+OCDAVError.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB0A45721B7F76800FAC4E9 /* NSError+OCDAVError.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCB0A45A21B7F76800FAC4E9 /* NSError+OCDAVError.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB0A45821B7F76800FAC4E9 /* NSError+OCDAVError.m */; }; DCB0A45C21B813A000FAC4E9 /* ExtensionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB0A45B21B813A000FAC4E9 /* ExtensionTests.m */; }; @@ -278,8 +284,8 @@ DCB0A46521B922A400FAC4E9 /* OCCoreConnectionStatusSignalProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB0A46321B922A400FAC4E9 /* OCCoreConnectionStatusSignalProvider.m */; }; DCB0A46821B9306300FAC4E9 /* OCCoreReachabilityConnectionStatusSignalProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB0A46621B9306300FAC4E9 /* OCCoreReachabilityConnectionStatusSignalProvider.h */; }; DCB0A46921B9306300FAC4E9 /* OCCoreReachabilityConnectionStatusSignalProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB0A46721B9306300FAC4E9 /* OCCoreReachabilityConnectionStatusSignalProvider.m */; }; - DCB0A46C21B9355C00FAC4E9 /* OCCoreMaintenanceModeStatusSignalProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB0A46A21B9355C00FAC4E9 /* OCCoreMaintenanceModeStatusSignalProvider.h */; }; - DCB0A46D21B9355C00FAC4E9 /* OCCoreMaintenanceModeStatusSignalProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB0A46B21B9355C00FAC4E9 /* OCCoreMaintenanceModeStatusSignalProvider.m */; }; + DCB0A46C21B9355C00FAC4E9 /* OCCoreServerStatusSignalProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB0A46A21B9355C00FAC4E9 /* OCCoreServerStatusSignalProvider.h */; }; + DCB0A46D21B9355C00FAC4E9 /* OCCoreServerStatusSignalProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB0A46B21B9355C00FAC4E9 /* OCCoreServerStatusSignalProvider.m */; }; DCB572AE2099EFC600B793CE /* OCDatabase+Schemas.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB572AC2099EFC600B793CE /* OCDatabase+Schemas.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCB572AF2099EFC600B793CE /* OCDatabase+Schemas.m in Sources */ = {isa = PBXBuildFile; fileRef = DCB572AD2099EFC600B793CE /* OCDatabase+Schemas.m */; }; DCC6563D20C979FB00110A97 /* TestTools.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC6563C20C979FB00110A97 /* TestTools.m */; }; @@ -609,8 +615,6 @@ DC2D646721C3D70F00EB26FD /* OCCore+Thumbnails.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCCore+Thumbnails.h"; sourceTree = ""; }; DC2D649A21C7B3DF00EB26FD /* OCAuthenticationMethodOAuth2+OCMocking.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCAuthenticationMethodOAuth2+OCMocking.h"; sourceTree = ""; }; DC2D649B21C7B3DF00EB26FD /* OCAuthenticationMethodOAuth2+OCMocking.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCAuthenticationMethodOAuth2+OCMocking.m"; sourceTree = ""; }; - DC2D701621CA633000EB26FD /* OCSyncRecordStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCSyncRecordStatus.h; sourceTree = ""; }; - DC2D701721CA633100EB26FD /* OCSyncRecordStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCSyncRecordStatus.m; sourceTree = ""; }; DC2EF1B520ECBB7100BA2A3E /* OCKeyValueStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCKeyValueStore.h; sourceTree = ""; }; DC2EF1B620ECBB7100BA2A3E /* OCKeyValueStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCKeyValueStore.m; sourceTree = ""; }; DC2EF1BE20ED782400BA2A3E /* OCConnectionQueue+BackgroundSessionRecovery.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCConnectionQueue+BackgroundSessionRecovery.h"; sourceTree = ""; }; @@ -672,7 +676,6 @@ DC594B1E21EFD7F900B882C4 /* BookmarkManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BookmarkManagerTests.m; sourceTree = ""; }; DC5A20302074E8890083DB7D /* CoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CoreTests.m; sourceTree = ""; }; DC5A20322074F9020083DB7D /* Ocean.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ocean.entitlements; sourceTree = SOURCE_ROOT; }; - DC5A203320750BEE0083DB7D /* OCCore+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCCore+Internal.h"; sourceTree = ""; }; DC5A794D21E5FAF20045BCAA /* OCConnection+Signals.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCConnection+Signals.m"; sourceTree = ""; }; DC62FD60211B11FD0034B628 /* OCItem+OCThumbnail.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCItem+OCThumbnail.h"; sourceTree = ""; }; DC62FD61211B11FD0034B628 /* OCItem+OCThumbnail.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCItem+OCThumbnail.m"; sourceTree = ""; }; @@ -735,6 +738,9 @@ DC7E0A88203732B3006111FA /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; DC812556210B548F00A9C037 /* OCTestTarget.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCTestTarget.h; sourceTree = ""; }; DC812557210B548F00A9C037 /* OCTestTarget.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCTestTarget.m; sourceTree = ""; }; + DC8245B121FB31E500775AB9 /* OCActivityManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCActivityManager.h; sourceTree = ""; }; + DC8245B221FB31E500775AB9 /* OCActivityManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCActivityManager.m; sourceTree = ""; }; + DC8245B921FB334200775AB9 /* OCCore+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCCore+Internal.h"; sourceTree = ""; }; DC8556EB204DEA2900189B9A /* OCConnectionDAVRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCConnectionDAVRequest.h; sourceTree = ""; }; DC8556EC204DEA2900189B9A /* OCConnectionDAVRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCConnectionDAVRequest.m; sourceTree = ""; }; DC8556EF204DEB9200189B9A /* OCXMLNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCXMLNode.h; sourceTree = ""; }; @@ -788,6 +794,12 @@ DCADC04C2072D54200DB8E83 /* OCSQLiteTableSchema.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCSQLiteTableSchema.m; sourceTree = ""; }; DCADC0502072DE6600DB8E83 /* OCSQLiteMigration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCSQLiteMigration.h; sourceTree = ""; }; DCADC0512072DE6600DB8E83 /* OCSQLiteMigration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCSQLiteMigration.m; sourceTree = ""; }; + DCAEB06721FA617D0067E147 /* OCActivity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCActivity.h; sourceTree = ""; }; + DCAEB06821FA617D0067E147 /* OCActivity.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCActivity.m; sourceTree = ""; }; + DCAEB06B21FA63D80067E147 /* OCSyncRecordActivity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCSyncRecordActivity.h; sourceTree = ""; }; + DCAEB06C21FA63D80067E147 /* OCSyncRecordActivity.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCSyncRecordActivity.m; sourceTree = ""; }; + DCAEB06F21FA67060067E147 /* OCActivityUpdate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCActivityUpdate.h; sourceTree = ""; }; + DCAEB07021FA67060067E147 /* OCActivityUpdate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCActivityUpdate.m; sourceTree = ""; }; DCB0A45721B7F76800FAC4E9 /* NSError+OCDAVError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSError+OCDAVError.h"; sourceTree = ""; }; DCB0A45821B7F76800FAC4E9 /* NSError+OCDAVError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSError+OCDAVError.m"; sourceTree = ""; }; DCB0A45B21B813A000FAC4E9 /* ExtensionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExtensionTests.m; sourceTree = ""; }; @@ -797,8 +809,8 @@ DCB0A46321B922A400FAC4E9 /* OCCoreConnectionStatusSignalProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCCoreConnectionStatusSignalProvider.m; sourceTree = ""; }; DCB0A46621B9306300FAC4E9 /* OCCoreReachabilityConnectionStatusSignalProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCCoreReachabilityConnectionStatusSignalProvider.h; sourceTree = ""; }; DCB0A46721B9306300FAC4E9 /* OCCoreReachabilityConnectionStatusSignalProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCCoreReachabilityConnectionStatusSignalProvider.m; sourceTree = ""; }; - DCB0A46A21B9355C00FAC4E9 /* OCCoreMaintenanceModeStatusSignalProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCCoreMaintenanceModeStatusSignalProvider.h; sourceTree = ""; }; - DCB0A46B21B9355C00FAC4E9 /* OCCoreMaintenanceModeStatusSignalProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCCoreMaintenanceModeStatusSignalProvider.m; sourceTree = ""; }; + DCB0A46A21B9355C00FAC4E9 /* OCCoreServerStatusSignalProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCCoreServerStatusSignalProvider.h; sourceTree = ""; }; + DCB0A46B21B9355C00FAC4E9 /* OCCoreServerStatusSignalProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCCoreServerStatusSignalProvider.m; sourceTree = ""; }; DCB572AC2099EFC600B793CE /* OCDatabase+Schemas.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCDatabase+Schemas.h"; sourceTree = ""; }; DCB572AD2099EFC600B793CE /* OCDatabase+Schemas.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCDatabase+Schemas.m"; sourceTree = ""; }; DCC6563B20C979FB00110A97 /* TestTools.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestTools.h; sourceTree = ""; }; @@ -1461,13 +1473,12 @@ DCC8F9D7202854FB00EB6701 /* OCCore.h */, DC3422812180765900705508 /* OCCore+ItemUpdates.m */, DC3422802180765900705508 /* OCCore+ItemUpdates.h */, - DC5A203320750BEE0083DB7D /* OCCore+Internal.h */, + DC8245B921FB334200775AB9 /* OCCore+Internal.h */, DCB0A46121B9227400FAC4E9 /* Connection Status */, DCADC04A2072D0FB00DB8E83 /* ItemList */, DC2D646221C3D60900EB26FD /* Thumbnails */, DC19BFE221CB9FAC007C20D1 /* FileProvider */, DC19BFE121CB9F4F007C20D1 /* Sync */, - DC855704204FE9E900189B9A /* Activity */, ); path = Core; sourceTree = ""; @@ -1475,8 +1486,14 @@ DC855704204FE9E900189B9A /* Activity */ = { isa = PBXGroup; children = ( - DC39DC58204215A800189B9A /* NSProgress+OCEvent.m */, - DC39DC57204215A800189B9A /* NSProgress+OCEvent.h */, + DC8245B221FB31E500775AB9 /* OCActivityManager.m */, + DC8245B121FB31E500775AB9 /* OCActivityManager.h */, + DCAEB06821FA617D0067E147 /* OCActivity.m */, + DCAEB06721FA617D0067E147 /* OCActivity.h */, + DCAEB07021FA67060067E147 /* OCActivityUpdate.m */, + DCAEB06F21FA67060067E147 /* OCActivityUpdate.h */, + DCAEB06C21FA63D80067E147 /* OCSyncRecordActivity.m */, + DCAEB06B21FA63D80067E147 /* OCSyncRecordActivity.h */, ); path = Activity; sourceTree = ""; @@ -1486,8 +1503,6 @@ children = ( DCC8FA24202B259D00EB6701 /* OCSyncRecord.m */, DCC8FA23202B259D00EB6701 /* OCSyncRecord.h */, - DC2D701721CA633100EB26FD /* OCSyncRecordStatus.m */, - DC2D701621CA633000EB26FD /* OCSyncRecordStatus.h */, ); path = Record; sourceTree = ""; @@ -1557,6 +1572,8 @@ DCC8FA2D202B405F00EB6701 /* OCEvent.h */, DCC8FA32202B443D00EB6701 /* OCEventTarget.m */, DCC8FA31202B443D00EB6701 /* OCEventTarget.h */, + DC39DC58204215A800189B9A /* NSProgress+OCEvent.m */, + DC39DC57204215A800189B9A /* NSProgress+OCEvent.h */, ); path = Events; sourceTree = ""; @@ -1716,13 +1733,13 @@ path = Reachability; sourceTree = ""; }; - DC98BDF121E73E4B003B5658 /* Maintenance Mode */ = { + DC98BDF121E73E4B003B5658 /* Server */ = { isa = PBXGroup; children = ( - DCB0A46B21B9355C00FAC4E9 /* OCCoreMaintenanceModeStatusSignalProvider.m */, - DCB0A46A21B9355C00FAC4E9 /* OCCoreMaintenanceModeStatusSignalProvider.h */, + DCB0A46B21B9355C00FAC4E9 /* OCCoreServerStatusSignalProvider.m */, + DCB0A46A21B9355C00FAC4E9 /* OCCoreServerStatusSignalProvider.h */, ); - path = "Maintenance Mode"; + path = Server; sourceTree = ""; }; DC98BDF221E73EB0003B5658 /* Network Path Monitor */ = { @@ -1810,7 +1827,7 @@ DCB0A46221B922A400FAC4E9 /* OCCoreConnectionStatusSignalProvider.h */, DC98BDF221E73EB0003B5658 /* Network Path Monitor */, DC98BDF021E73E36003B5658 /* Reachability */, - DC98BDF121E73E4B003B5658 /* Maintenance Mode */, + DC98BDF121E73E4B003B5658 /* Server */, ); path = "Connection Status"; sourceTree = ""; @@ -1873,6 +1890,7 @@ DC1D4D3420DBD557005A3DFC /* File Handling */, DC85570D204FEAC500189B9A /* Events */, DC85570E204FEAE900189B9A /* Vaults */, + DC855704204FE9E900189B9A /* Activity */, DC855702204FE9CA00189B9A /* Core */, DC19BFF321CBE293007C20D1 /* Wait Condition */, DC855710204FEB1500189B9A /* Query */, @@ -2023,7 +2041,6 @@ DC1889802189EC2600CFB3F9 /* OCLogWriter.h in Headers */, DC00DB1D219B120300C82737 /* OCConnectionDAVMultistatusResponse.h in Headers */, DCC6567820CA696A00110A97 /* OCBookmarkManager.h in Headers */, - DC2D701821CA633100EB26FD /* OCSyncRecordStatus.h in Headers */, DC680585212EC27B006C3B1F /* OCExtension+License.h in Headers */, DC07C296212450DC00B815A4 /* OCExtensionLocation.h in Headers */, DC446C85206B8DB500189B9A /* OCRunLoopThread.h in Headers */, @@ -2049,7 +2066,8 @@ DC8913642092088600028999 /* NSString+OCVersionCompare.h in Headers */, DCC8F9E62028556500EB6701 /* OCConnection.h in Headers */, DC24F8E821E2B3EF00C9119C /* OCWaitConditionIssue.h in Headers */, - DCB0A46C21B9355C00FAC4E9 /* OCCoreMaintenanceModeStatusSignalProvider.h in Headers */, + DCB0A46C21B9355C00FAC4E9 /* OCCoreServerStatusSignalProvider.h in Headers */, + DCAEB06D21FA63D80067E147 /* OCSyncRecordActivity.h in Headers */, DCEA7D972093556600F25223 /* OCCache.h in Headers */, DC179CD520948DF30018DF7F /* NSProgress+OCExtensions.h in Headers */, DCADC0442072CCC900DB8E83 /* OCCoreItemListTask.h in Headers */, @@ -2074,11 +2092,13 @@ DCC8F9E22028554E00EB6701 /* OCBookmark.h in Headers */, DC20DE4B21BFCBC20096000B /* OCLogComponent.h in Headers */, DCADC0482072CDEA00DB8E83 /* OCCoreItemList.h in Headers */, + DC8245BA21FB334200775AB9 /* OCCore+Internal.h in Headers */, DCEEB2E52044B0A400189B9A /* OCAuthenticationMethod+OCTools.h in Headers */, DC39DC462041A03300189B9A /* OCAuthenticationMethodBasicAuth.h in Headers */, DC85571C2050196000189B9A /* OCItem+OCXMLObjectCreation.h in Headers */, DC24F8F021E4F5BF00C9119C /* OCSQLiteDB+Internal.h in Headers */, DC14CC4A21067320006DDA69 /* OCCore+ItemList.h in Headers */, + DCAEB07121FA67060067E147 /* OCActivityUpdate.h in Headers */, DC594B1121EF4B2900B882C4 /* OCAsyncSequentialQueue.h in Headers */, DC4E0A5820927048007EB05F /* OCItemVersionIdentifier.h in Headers */, DCEEB2DC20430B1400189B9A /* NSURL+OCURLQueryParameterExtensions.h in Headers */, @@ -2130,14 +2150,15 @@ DC139CD020DBC1690090175A /* OCChecksumAlgorithmSHA1.h in Headers */, DC19BFCA21CA6B91007C20D1 /* OCSyncIssue.h in Headers */, DC708CE8214135FE00FE43CA /* OCSyncActionUpload.h in Headers */, + DC8245B321FB31E500775AB9 /* OCActivityManager.h in Headers */, DCB0A45921B7F76800FAC4E9 /* NSError+OCDAVError.h in Headers */, + DCAEB06921FA617D0067E147 /* OCActivity.h in Headers */, DCFFF57E20D3A51C0096D2D3 /* OCSyncContext.h in Headers */, DC0283632090A3E8005B6334 /* OCItemThumbnail.h in Headers */, DCADC04D2072D54200DB8E83 /* OCSQLiteTableSchema.h in Headers */, DC20DE5021BFCEB00096000B /* OCLogToggle.h in Headers */, DCC8F9EA2028557100EB6701 /* OCDatabase.h in Headers */, DC02835B209098D7005B6334 /* OCImage.h in Headers */, - DC5A203420750BEE0083DB7D /* OCCore+Internal.h in Headers */, DC3422822180765900705508 /* OCCore+ItemUpdates.h in Headers */, DCC8F9F22028559600EB6701 /* OCItem.h in Headers */, DC708CDC214135C000FE43CA /* OCSyncActionCreateFolder.h in Headers */, @@ -2478,11 +2499,12 @@ buildActionMask = 2147483647; files = ( DCE2661D2113323C0001FB2C /* OCCore+CommandDownload.m in Sources */, - DCB0A46D21B9355C00FAC4E9 /* OCCoreMaintenanceModeStatusSignalProvider.m in Sources */, + DCB0A46D21B9355C00FAC4E9 /* OCCoreServerStatusSignalProvider.m in Sources */, DCEEB2F22047094500189B9A /* NSData+OCHash.m in Sources */, DC72E4332063DD7600189B9A /* OCClassSettingsFlatSourcePropertyList.m in Sources */, DC4AFAB9206AE92F00189B9A /* OCSQLiteTransaction.m in Sources */, DCFFF57F20D3A51C0096D2D3 /* OCSyncContext.m in Sources */, + DCAEB06A21FA617D0067E147 /* OCActivity.m in Sources */, DC68057E212EB438006C3B1F /* OCExtensionMatch.m in Sources */, DCE26621211348B00001FB2C /* OCCore+CommandLocalImport.m in Sources */, DCDBEE302048A71200189B9A /* OCConnection+Tools.m in Sources */, @@ -2499,6 +2521,7 @@ DCC8FA0020285C1500EB6701 /* OCAuthenticationMethodOAuth2.m in Sources */, DC54396520D50B8A002BF291 /* OCCore+CommandDelete.m in Sources */, DC98BDF621E73ECE003B5658 /* OCCoreNetworkPathMonitorSignalProvider.m in Sources */, + DC8245B421FB31E500775AB9 /* OCActivityManager.m in Sources */, DC3422832180765900705508 /* OCCore+ItemUpdates.m in Sources */, DC8556FC204F4F3000189B9A /* OCXMLParser.m in Sources */, DC85980820D8F5C000A433C6 /* OCCore+CommandCopyMove.m in Sources */, @@ -2542,10 +2565,10 @@ DCADC0492072CDEA00DB8E83 /* OCCoreItemList.m in Sources */, DCDBEE392049EF3C00189B9A /* NSURL+OCURLNormalization.m in Sources */, DCC8F9DA202854FB00EB6701 /* OCCore.m in Sources */, + DCAEB07221FA67060067E147 /* OCActivityUpdate.m in Sources */, DCC8F9EB2028557100EB6701 /* OCDatabase.m in Sources */, DC19BFCB21CA6B91007C20D1 /* OCSyncIssue.m in Sources */, DCA91F3021A0BDE400AEDFB4 /* OCSyncAction+FileProvider.m in Sources */, - DC2D701921CA633100EB26FD /* OCSyncRecordStatus.m in Sources */, DCADC0532072DE6600DB8E83 /* OCSQLiteMigration.m in Sources */, DC545E7B203F7DD0006111FA /* OCUser.m in Sources */, DC594B1221EF4B2900B882C4 /* OCAsyncSequentialQueue.m in Sources */, @@ -2591,6 +2614,7 @@ DC19BFEA21CBACB0007C20D1 /* OCProcessSession.m in Sources */, DC8556F7204F361100189B9A /* OCLogger.m in Sources */, DC24F8E921E2B3EF00C9119C /* OCWaitConditionIssue.m in Sources */, + DCAEB06E21FA63D80067E147 /* OCSyncRecordActivity.m in Sources */, DCB0A46921B9306300FAC4E9 /* OCCoreReachabilityConnectionStatusSignalProvider.m in Sources */, DC179CD620948DF30018DF7F /* NSProgress+OCExtensions.m in Sources */, DCC6567920CA696A00110A97 /* OCBookmarkManager.m in Sources */, diff --git a/ownCloudSDK/Activity/OCActivity.h b/ownCloudSDK/Activity/OCActivity.h new file mode 100644 index 00000000..a05cfbe1 --- /dev/null +++ b/ownCloudSDK/Activity/OCActivity.h @@ -0,0 +1,91 @@ +// +// OCActivity.h +// ownCloudSDK +// +// Created by Felix Schwarz on 24.01.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 "OCIssue.h" + +typedef NSString* OCActivityIdentifier; + +typedef NS_ENUM(NSUInteger, OCActivityState) +{ + OCActivityStatePending, //!< Activity is pending + OCActivityStateRunning, //!< Activity is being executed + OCActivityStatePaused, //!< Activity is paused + OCActivityStateFailed //!< Activity has failed (and optionally awaits resolution) +}; + +NS_ASSUME_NONNULL_BEGIN + +@class OCActivity; +@class OCActivityUpdate; + +@protocol OCActivitySource + +@required +@property(readonly,nonatomic) OCActivityIdentifier activityIdentifier; //!< Returns an identifier uniquely identifying the source's activity. + +@optional +- (OCActivity *)provideActivity; //!< Returns a new instance of OCActivity representing the source's activity. + +@end + +@interface OCActivity : NSObject +{ + OCActivityIdentifier _identifier; + + OCActivityState _state; + + NSInteger _ranking; + + NSString *_localizedDescription; + NSString *_localizedStatusMessage; + + NSProgress *_progress; + + OCIssue *_issue; + + BOOL _isCancellable; +} + +@property(strong) OCActivityIdentifier identifier; //!< Identifier uniquely identifying an activity + +@property(assign,nonatomic) OCActivityState state; //!< State of the activity + +@property(assign) NSInteger ranking; //!< Lower numbers for higher prioritized items + +@property(strong) NSString *localizedDescription; //!< Localized description of the activity (f.ex. "Copying party.jpg to Photos..") +@property(nullable,strong) NSString *localizedStatusMessage; //!< Localized message describing the status of the activity (f.ex. "Waiting for response..") + +@property(nullable,strong) NSProgress *progress; //!< Progress information on the activity + +@property(nullable,strong) OCIssue *issue; //!< If .state is failed, an issue that can be used to resolve the failure (optional) + +@property(assign) BOOL isCancellable; //!< If YES, the activity can be cancelled + ++ (instancetype)withIdentifier:(OCActivityIdentifier)identifier description:(NSString *)description statusMessage:(nullable NSString *)statusMessage ranking:(NSInteger)ranking; + +- (instancetype)initWithIdentifier:(OCActivityIdentifier)identifier; + +- (NSError *)applyUpdate:(OCActivityUpdate *)update; //!< Applies an update to the activity. Returns nil if the update could be applied, an error otherwise. +- (NSError *)applyValue:(nullable id )value forKeyPath:(NSString *)keyPath; //!< Applies a new value to a keypath (entrypoint for subclassing) + +- (void)cancel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/Activity/OCActivity.m b/ownCloudSDK/Activity/OCActivity.m new file mode 100644 index 00000000..b6220b7d --- /dev/null +++ b/ownCloudSDK/Activity/OCActivity.m @@ -0,0 +1,103 @@ +// +// OCActivity.m +// ownCloudSDK +// +// Created by Felix Schwarz on 24.01.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 "OCActivity.h" +#import "OCActivityUpdate.h" +#import "NSError+OCError.h" + +@implementation OCActivity + +@synthesize identifier = _identifier; + +@synthesize ranking = _ranking; + +@synthesize localizedDescription = _localizedDescription; +@synthesize localizedStatusMessage = _localizedStatusMessage; + +@synthesize progress = _progress; + +@synthesize issue = _issue; + +@synthesize isCancellable = _isCancellable; + ++ (instancetype)withIdentifier:(OCActivityIdentifier)identifier description:(NSString *)description statusMessage:(nullable NSString *)statusMessage ranking:(NSInteger)ranking +{ + OCActivity *activity = [OCActivity new]; + + activity.identifier = identifier; + activity.localizedDescription = description; + activity.localizedStatusMessage = statusMessage; + activity.ranking = ranking; + + return (activity); +} + +- (instancetype)initWithIdentifier:(OCActivityIdentifier)identifier +{ + if ((self = [super init]) != nil) + { + _identifier = identifier; + } + + return (self); +} + +- (void)cancel +{ + if (_isCancellable) + { + [self.progress cancel]; + } +} + +- (NSError *)applyUpdate:(OCActivityUpdate *)update +{ + __block NSError *error = nil; + + if (update.type != OCActivityUpdateTypeProperty) + { + return (OCError(OCErrorInsufficientParameters)); + } + + [update.updatesByKeyPath enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull keyPath, id _Nonnull value, BOOL * _Nonnull stop) { + NSError *applicationError = nil; + + if ([value isEqual:[NSNull null]]) + { + value = nil; + } + + if ((applicationError = [self applyValue:value forKeyPath:keyPath]) != nil) + { + error = applicationError; + *stop = YES; + } + }]; + + return (error); +} + +- (NSError *)applyValue:(nullable id )value forKeyPath:(NSString *)keyPath +{ + // Entrypoint for subclassing + [self setValue:value forKeyPath:keyPath]; + + return (nil); +} + +@end diff --git a/ownCloudSDK/Activity/OCActivityManager.h b/ownCloudSDK/Activity/OCActivityManager.h new file mode 100644 index 00000000..46eb5d12 --- /dev/null +++ b/ownCloudSDK/Activity/OCActivityManager.h @@ -0,0 +1,47 @@ +// +// OCActivityManager.h +// ownCloudSDK +// +// Created by Felix Schwarz on 25.01.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 "OCActivity.h" +#import "OCLogTag.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCActivityManager : NSObject + +#pragma mark - Init +- (instancetype)initWithUpdateNotificationName:(NSString *)updateNotificationName; + +#pragma mark - Update notifications +@property(readonly,nonatomic) NSNotificationName activityUpdateNotificationName; + +#pragma mark - Access +@property(readonly,nonatomic) NSArray *activities; +- (nullable OCActivity *)activityForIdentifier:(OCActivityIdentifier)activityIdentifier; + +#pragma mark - Updating +- (void)update:(OCActivityUpdate *)update; + +@end + +extern NSString *OCActivityManagerNotificationUserInfoUpdatesKey; //!< UserInfo key that contains an array of dictionaries providing info on the activity updates: + +extern NSString *OCActivityManagerUpdateTypeKey; //!< the type of the update [OCActivityUpdateType] +extern NSString *OCActivityManagerUpdateActivityKey; //!< the updated activity object [OCActivity] + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/Activity/OCActivityManager.m b/ownCloudSDK/Activity/OCActivityManager.m new file mode 100644 index 00000000..5ba7f7ba --- /dev/null +++ b/ownCloudSDK/Activity/OCActivityManager.m @@ -0,0 +1,270 @@ +// +// OCActivityManager.m +// ownCloudSDK +// +// Created by Felix Schwarz on 25.01.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 "OCActivityManager.h" +#import "OCActivityUpdate.h" +#import "OCLogger.h" + +@interface OCActivityManager () +{ + NSMutableArray *_activities; + NSMutableDictionary *_activityByIdentifier; + NSArray *_exposedActivities; + NSMutableArray > *> *_queuedActivityUpdates; + NSNotificationName _activityUpdateNotificationName; +} + +@end + +@implementation OCActivityManager + +#pragma mark - Init & Dealloc +- (instancetype)initWithUpdateNotificationName:(NSString *)updateNotificationName +{ + if ((self = [super init]) != nil) + { + _activities = [NSMutableArray new]; + _activityByIdentifier = [NSMutableDictionary new]; + _queuedActivityUpdates = [NSMutableArray new]; + _activityUpdateNotificationName = updateNotificationName; + } + + return(self); +} + +- (NSNotificationName)activityUpdateNotificationName +{ + return (_activityUpdateNotificationName); +} + +- (NSArray *)activities +{ + NSArray *activities = nil; + + @synchronized (_activities) + { + if (_exposedActivities == nil) + { + _exposedActivities = [[NSArray alloc] initWithArray:_activities]; + } + + activities = _exposedActivities; + } + + return (activities); +} + +- (void)update:(OCActivityUpdate *)update +{ + __block OCActivity *updatedActivity = nil; + + switch (update.type) + { + case OCActivityUpdateTypePublish: { + OCActivity *newActivity = update.activity; + + if (newActivity == nil) + { + OCLogError(@"Activity missing from Publish-ActivityUpdate: %@", update); + } + else if (newActivity.identifier == nil) + { + OCLogError(@"Publish-ActivityUpdate: activity lacks identifer: %@", update); + } + else + { + @synchronized(_activities) + { + [_activities addObject:newActivity]; + [_activities sortUsingDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"ranking" ascending:YES] ]]; + _activityByIdentifier[newActivity.identifier] = newActivity; + + _exposedActivities = nil; + } + + updatedActivity = newActivity; + } + } + break; + + case OCActivityUpdateTypeProperty: { + OCActivityIdentifier activityIdentifier = update.identifier; + + if (activityIdentifier == nil) + { + OCLogError(@"Property-ActivityUpdate: update lacks identifer: %@", update); + } + else + { + OCActivity *activity = nil; + + @synchronized (_activities) + { + if ((activity = _activityByIdentifier[activityIdentifier]) != nil) + { + [activity applyUpdate:update]; + + updatedActivity = activity; + } + } + } + } + break; + + case OCActivityUpdateTypeUnpublish: { + OCActivityIdentifier activityIdentifier = update.identifier; + + if (activityIdentifier == nil) + { + OCLogError(@"Unpublish-ActivityUpdate: update lacks identifer: %@", update); + } + else + { + OCActivity *unpublishedActivity = nil; + + @synchronized (_activities) + { + if ((unpublishedActivity = _activityByIdentifier[activityIdentifier]) != nil) + { + [_activityByIdentifier removeObjectForKey:activityIdentifier]; + [_activities removeObjectIdenticalTo:unpublishedActivity]; + + _exposedActivities = nil; + } + } + + updatedActivity = unpublishedActivity; + } + } + break; + } + + if (updatedActivity != nil) + { + __block NSDictionary *activityUpdateDict = @{ + OCActivityManagerUpdateTypeKey : @(update.type), + OCActivityManagerUpdateActivityKey : updatedActivity + }; + + @synchronized(_queuedActivityUpdates) + { + __block NSMutableIndexSet *removeUpdatesIndexes = nil; + + switch (update.type) + { + case OCActivityUpdateTypeUnpublish: + // If an activity is unpublished, we can remove all previous updates regaring it + [_queuedActivityUpdates enumerateObjectsUsingBlock:^(NSDictionary> * _Nonnull updateDict, NSUInteger idx, BOOL * _Nonnull stop) { + if (updateDict[OCActivityManagerUpdateActivityKey] == updatedActivity) + { + if (((NSNumber *)updateDict[OCActivityManagerUpdateTypeKey]).integerValue == OCActivityUpdateTypePublish) + { + // If the activity has not yet been published.. no need to notify about its removal now + activityUpdateDict = nil; + } + + if (removeUpdatesIndexes == nil) + { + removeUpdatesIndexes = [NSMutableIndexSet new]; + } + + [removeUpdatesIndexes addIndex:idx]; + } + }]; + break; + + case OCActivityUpdateTypeProperty: + // Only add property update if there's no other update (or publish) update in the queue already + [_queuedActivityUpdates enumerateObjectsUsingBlock:^(NSDictionary> * _Nonnull updateDict, NSUInteger idx, BOOL * _Nonnull stop) { + if (updateDict[OCActivityManagerUpdateActivityKey] == updatedActivity) + { + activityUpdateDict = nil; + } + }]; + break; + + case OCActivityUpdateTypePublish: + // Nothing to do here + break; + } + + if (removeUpdatesIndexes != nil) + { + [_queuedActivityUpdates removeObjectsAtIndexes:removeUpdatesIndexes]; + } + + if (activityUpdateDict != nil) + { + [_queuedActivityUpdates addObject:activityUpdateDict]; + } + } + + @synchronized(_queuedActivityUpdates) + { + if (_queuedActivityUpdates.count > 0) + { + dispatch_async(dispatch_get_main_queue(), ^{ + NSDictionary *userInfo = nil; + + @synchronized(self->_queuedActivityUpdates) + { + if (self->_queuedActivityUpdates.count > 0) + { + userInfo = @{ + OCActivityManagerNotificationUserInfoUpdatesKey : [[NSArray alloc] initWithArray:self->_queuedActivityUpdates] + }; + + [self->_queuedActivityUpdates removeAllObjects]; + } + } + + if (userInfo != nil) + { + [[NSNotificationCenter defaultCenter] postNotificationName:self.activityUpdateNotificationName object:nil userInfo:userInfo]; + } + }); + } + } + } +} + +- (nullable OCActivity *)activityForIdentifier:(OCActivityIdentifier)activityIdentifier +{ + @synchronized(_activities) + { + return(_activityByIdentifier[activityIdentifier]); + } +} + +#pragma mark - Log tagging ++ (nonnull NSArray *)logTags +{ + return (@[@"ACTIVITY"]); +} + +- (nonnull NSArray *)logTags +{ + return (@[@"ACTIVITY" , OCLogTagTypedID(@"ActivityNotificationName", _activityUpdateNotificationName)]); +} + +@end + +NSString *OCActivityManagerNotificationUserInfoUpdatesKey = @"updates"; + +NSString *OCActivityManagerUpdateTypeKey = @"updateType"; +NSString *OCActivityManagerUpdateActivityKey = @"activity"; diff --git a/ownCloudSDK/Activity/OCActivityUpdate.h b/ownCloudSDK/Activity/OCActivityUpdate.h new file mode 100644 index 00000000..80768355 --- /dev/null +++ b/ownCloudSDK/Activity/OCActivityUpdate.h @@ -0,0 +1,60 @@ +// +// OCActivityUpdate.h +// ownCloudSDK +// +// Created by Felix Schwarz on 24.01.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 "OCActivity.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger,OCActivityUpdateType) +{ + OCActivityUpdateTypePublish, + OCActivityUpdateTypeProperty, + OCActivityUpdateTypeUnpublish +}; + +@interface OCActivityUpdate : NSObject +{ + OCActivityUpdateType _type; + OCActivityIdentifier _identifier; + NSMutableDictionary > *_updatesByKeyPath; +} + +@property(readonly) OCActivityUpdateType type; //!< The type of activity + +@property(readonly,strong) OCActivityIdentifier identifier; + +@property(readonly,strong) OCActivity *activity; + +@property(readonly,strong) NSDictionary > *updatesByKeyPath; + ++ (instancetype)publishingActivity:(OCActivity *)activity; ++ (instancetype)unpublishActivityForIdentifier:(OCActivityIdentifier)identifier; ++ (instancetype)updatingActivityForIdentifier:(OCActivityIdentifier)identifier; + ++ (instancetype)publishingActivityFor:(id)source; ++ (instancetype)unpublishActivityFor:(id)source; ++ (instancetype)updatingActivityFor:(id)source; + +- (instancetype)withStatusMessage:(NSString *)statusMessage; +- (instancetype)withProgress:(NSProgress *)progress; +- (instancetype)withState:(OCActivityState)state; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/Activity/OCActivityUpdate.m b/ownCloudSDK/Activity/OCActivityUpdate.m new file mode 100644 index 00000000..4b32db06 --- /dev/null +++ b/ownCloudSDK/Activity/OCActivityUpdate.m @@ -0,0 +1,104 @@ +// +// OCActivityUpdate.m +// ownCloudSDK +// +// Created by Felix Schwarz on 24.01.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 "OCActivityUpdate.h" + +@implementation OCActivityUpdate + +@synthesize type = _type; +@synthesize identifier = _identifier; +@synthesize updatesByKeyPath = _updatesByKeyPath; + ++ (instancetype)publishingActivity:(OCActivity *)activity +{ + OCActivityUpdate *update = [OCActivityUpdate new]; + + update->_type = OCActivityUpdateTypePublish; + update->_activity = activity; + update->_identifier = activity.identifier; + + return (update); +} + ++ (instancetype)unpublishActivityForIdentifier:(OCActivityIdentifier)identifier +{ + OCActivityUpdate *update = [OCActivityUpdate new]; + + update->_type = OCActivityUpdateTypeUnpublish; + update->_identifier = identifier; + + return (update); +} + ++ (instancetype)updatingActivityForIdentifier:(OCActivityIdentifier)identifier +{ + OCActivityUpdate *update = [OCActivityUpdate new]; + + update->_type = OCActivityUpdateTypeProperty; + update->_identifier = identifier; + + return (update); +} + ++ (instancetype)publishingActivityFor:(id)source +{ + return ([self publishingActivity:[source provideActivity]]); +} + ++ (instancetype)unpublishActivityFor:(id)source +{ + return ([self unpublishActivityForIdentifier:source.activityIdentifier]); +} + ++ (instancetype)updatingActivityFor:(id)source; +{ + return ([self updatingActivityForIdentifier:source.activityIdentifier]); +} + +- (instancetype)init +{ + if ((self = [super init]) != nil) + { + _updatesByKeyPath = [NSMutableDictionary new]; + } + + return(self); +} + +- (instancetype)withStatusMessage:(NSString *)statusMessage +{ + _updatesByKeyPath[@"localizedStatusMessage"] = statusMessage; + + return (self); +} + +- (instancetype)withProgress:(NSProgress *)progress +{ + _updatesByKeyPath[@"progress"] = progress; + + return (self); +} + +- (instancetype)withState:(OCActivityState)state +{ + _updatesByKeyPath[@"state"] = @(state); + + return (self); +} + +@end diff --git a/ownCloudSDK/Activity/OCSyncRecordActivity.h b/ownCloudSDK/Activity/OCSyncRecordActivity.h new file mode 100644 index 00000000..e2029f70 --- /dev/null +++ b/ownCloudSDK/Activity/OCSyncRecordActivity.h @@ -0,0 +1,44 @@ +// +// OCSyncRecordActivity.h +// ownCloudSDK +// +// Created by Felix Schwarz on 24.01.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 "OCActivity.h" +#import "OCCore.h" +#import "OCSyncRecord.h" +#import "OCTypes.h" +#import "OCLogTag.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCSyncRecordActivity : OCActivity + +@property(strong) OCSyncRecordID recordID; + +@property(assign) OCEventType type; +@property(assign,nonatomic) OCSyncRecordState recordState; + +- (instancetype)initWithSyncRecord:(OCSyncRecord *)syncRecord identifier:(OCActivityIdentifier)identifier; + +@end + +@interface OCActivityUpdate (OCSyncRecord) + +- (instancetype)withRecordState:(OCSyncRecordState)recordState; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/Activity/OCSyncRecordActivity.m b/ownCloudSDK/Activity/OCSyncRecordActivity.m new file mode 100644 index 00000000..e821917f --- /dev/null +++ b/ownCloudSDK/Activity/OCSyncRecordActivity.m @@ -0,0 +1,81 @@ +// +// OCSyncRecordActivity.m +// ownCloudSDK +// +// Created by Felix Schwarz on 24.01.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 "OCSyncRecordActivity.h" +#import "OCSyncAction.h" + +@implementation OCSyncRecordActivity + +- (instancetype)initWithSyncRecord:(OCSyncRecord *)syncRecord identifier:(OCActivityIdentifier)identifier +{ + if ((self = [super initWithIdentifier:identifier]) != nil) + { + _recordID = syncRecord.recordID; + _type = syncRecord.action.actionEventType; + self.recordState = syncRecord.state; + + _ranking = syncRecord.recordID.integerValue; + _progress = syncRecord.progress; + + _localizedDescription = syncRecord.action.localizedDescription; + } + + return (self); +} + +- (void)setRecordState:(OCSyncRecordState)recordState +{ + if ((_recordState != recordState) || (_localizedStatusMessage == nil)) + { + _recordState = recordState; + + switch (_recordState) + { + case OCSyncRecordStatePending: + case OCSyncRecordStateReady: + self.state = OCActivityStatePending; + self.localizedStatusMessage = OCLocalized(@"Pending"); + break; + + case OCSyncRecordStateProcessing: + case OCSyncRecordStateCompleted: + self.state = OCActivityStateRunning; + self.localizedStatusMessage = OCLocalized(@"Running"); + break; + + case OCSyncRecordStateFailed: + self.state = OCActivityStateFailed; + self.localizedStatusMessage = OCLocalized(@"Failed"); + break; + } + } +} + +@end + +@implementation OCActivityUpdate (OCSyncRecord) + +- (instancetype)withRecordState:(OCSyncRecordState)recordState +{ + _updatesByKeyPath[@"recordState"] = @(recordState); + + return (self); +} + +@end + diff --git a/ownCloudSDK/Bookmark/OCBookmark.h b/ownCloudSDK/Bookmark/OCBookmark.h index ea51bb24..1b2867eb 100644 --- a/ownCloudSDK/Bookmark/OCBookmark.h +++ b/ownCloudSDK/Bookmark/OCBookmark.h @@ -64,4 +64,6 @@ NS_ASSUME_NONNULL_BEGIN extern NSNotificationName OCBookmarkAuthenticationDataChangedNotification; //!< Name of notification that is sent whenever a bookmark's authenticationData is changed. The object of the notification is the bookmark. Sent only if .authenticationDataStorage is OCBookmarkAuthenticationDataStorageKeychain. +extern NSNotificationName OCBookmarkUpdatedNotification; //!< Name of notification that can be sent by third parties after completing an update to a bookmark. + NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/Bookmark/OCBookmark.m b/ownCloudSDK/Bookmark/OCBookmark.m index 3e2288fb..275c7c93 100644 --- a/ownCloudSDK/Bookmark/OCBookmark.m +++ b/ownCloudSDK/Bookmark/OCBookmark.m @@ -247,3 +247,4 @@ - (id)copyWithZone:(NSZone *)zone @end NSNotificationName OCBookmarkAuthenticationDataChangedNotification = @"OCBookmarkAuthenticationDataChanged"; +NSNotificationName OCBookmarkUpdatedNotification = @"OCBookmarkUpdatedNotification"; diff --git a/ownCloudSDK/Connection/OCConnection.h b/ownCloudSDK/Connection/OCConnection.h index 1f0e1139..d752d908 100644 --- a/ownCloudSDK/Connection/OCConnection.h +++ b/ownCloudSDK/Connection/OCConnection.h @@ -70,7 +70,7 @@ typedef NS_ENUM(NSUInteger, OCConnectionRequestInstruction) - (void)connectionChangedState:(OCConnection *)connection; -- (OCConnectionRequestInstruction)connection:(OCConnection *)connection instructionForFinishedRequest:(OCConnectionRequest *)finishedRequest defaultsTo:(OCConnectionRequestInstruction)defaulInstruction; +- (OCConnectionRequestInstruction)connection:(OCConnection *)connection instructionForFinishedRequest:(OCConnectionRequest *)finishedRequest error:(NSError *)error defaultsTo:(OCConnectionRequestInstruction)defaultInstruction; @end @@ -183,7 +183,7 @@ typedef NS_ENUM(NSUInteger, OCConnectionRequestInstruction) - (void)finishedQueueForResumedBackgroundSessionWithIdentifier:(NSString *)backgroundSessionIdentifier; #pragma mark - Rescheduling support -- (OCConnectionRequestInstruction)instructionForFinishedRequest:(OCConnectionRequest *)finishedRequest; +- (OCConnectionRequestInstruction)instructionForFinishedRequest:(OCConnectionRequest *)finishedRequest error:(NSError *)error; @end diff --git a/ownCloudSDK/Connection/OCConnection.m b/ownCloudSDK/Connection/OCConnection.m index 5857302a..223b7bea 100644 --- a/ownCloudSDK/Connection/OCConnection.m +++ b/ownCloudSDK/Connection/OCConnection.m @@ -311,6 +311,8 @@ - (void)handleValidationOfRequest:(OCConnectionRequest *)request certificate:(OC self->_bookmark.certificate = request.responseCertificate; self->_bookmark.certificateModificationDate = [NSDate date]; + + [[NSNotificationCenter defaultCenter] postNotificationName:OCBookmarkUpdatedNotification object:self->_bookmark]; } }]]; } @@ -896,6 +898,13 @@ - (NSProgress *)uploadFileFromURL:(NSURL *)sourceURL withName:(NSString *)fileNa } } + if (![[NSFileManager defaultManager] fileExistsAtPath:sourceURL.path]) + { + [eventTarget handleError:OCError(OCErrorFileNotFound) type:OCEventTypeUpload sender:self]; + + return(nil); + } + if ((uploadURL = [[[self URLForEndpoint:OCConnectionEndpointIDWebDAVRoot options:nil] URLByAppendingPathComponent:newParentDirectory.path] URLByAppendingPathComponent:fileName]) != nil) { OCConnectionRequest *request = [OCConnectionRequest requestWithURL:uploadURL]; @@ -965,6 +974,12 @@ - (NSProgress *)uploadFileFromURL:(NSURL *)sourceURL withName:(NSString *)fileNa [request setValue:checksumHeaderValue forHeaderField:@"OC-Checksum"]; } + if ((sourceURL == nil) || (fileName == nil) || (newParentDirectory == nil) || (modDate == nil) || (fileSize == nil)) + { + [eventTarget handleError:OCError(OCErrorInsufficientParameters) type:OCEventTypeUpload sender:self]; + return(nil); + } + // Set meta data for handling request.requiredSignals = self.actionSignals; request.resultHandlerAction = @selector(_handleUploadFileResult:error:); @@ -1381,6 +1396,7 @@ - (void)_handleCreateFolderResult:(OCConnectionRequest *)request error:(NSError OCItem *newFolderItem = items.firstObject; newFolderItem.parentFileID = parentItem.fileID; + newFolderItem.parentLocalID = parentItem.localID; if (error == nil) { @@ -1524,6 +1540,7 @@ - (void)_handleCopyMoveItemResult:(OCConnectionRequest *)request error:(NSError OCItem *newItem = items.firstObject; newItem.parentFileID = parentItem.fileID; + newItem.parentLocalID = parentItem.localID; if (error == nil) { @@ -1934,13 +1951,13 @@ - (void)finishedQueueForResumedBackgroundSessionWithIdentifier:(NSString *)backg } #pragma mark - Rescheduling support -- (OCConnectionRequestInstruction)instructionForFinishedRequest:(OCConnectionRequest *)finishedRequest +- (OCConnectionRequestInstruction)instructionForFinishedRequest:(OCConnectionRequest *)finishedRequest error:(NSError *)error { OCConnectionRequestInstruction instruction = OCConnectionRequestInstructionDeliver; - if ((_delegate!=nil) && [_delegate respondsToSelector:@selector(connection:instructionForFinishedRequest:defaultsTo:)]) + if ((_delegate!=nil) && [_delegate respondsToSelector:@selector(connection:instructionForFinishedRequest:error:defaultsTo:)]) { - instruction = [_delegate connection:self instructionForFinishedRequest:finishedRequest defaultsTo:instruction]; + instruction = [_delegate connection:self instructionForFinishedRequest:finishedRequest error:error defaultsTo:instruction]; } return (instruction); diff --git a/ownCloudSDK/Connection/OCConnectionQueue.m b/ownCloudSDK/Connection/OCConnectionQueue.m index 8f734666..cb4d3464 100644 --- a/ownCloudSDK/Connection/OCConnectionQueue.m +++ b/ownCloudSDK/Connection/OCConnectionQueue.m @@ -499,6 +499,7 @@ - (void)handleFinishedRequest:(OCConnectionRequest *)request error:(NSError *)er - (void)_handleFinishedRequest:(OCConnectionRequest *)request error:(NSError *)error scheduleQueuedRequests:(BOOL)scheduleQueuedRequests { BOOL reschedulingAllowed = scheduleQueuedRequests; + NSError *inError = error; if (request==nil) { return; } @@ -577,7 +578,7 @@ - (void)_handleFinishedRequest:(OCConnectionRequest *)request error:(NSError *)e if ((_connection!=nil) && reschedulingAllowed) { - requestInstruction = [_connection instructionForFinishedRequest:request]; + requestInstruction = [_connection instructionForFinishedRequest:request error:inError]; } if (requestInstruction == OCConnectionRequestInstructionDeliver) diff --git a/ownCloudSDK/Core/Connection Status/OCCore+ConnectionStatus.m b/ownCloudSDK/Core/Connection Status/OCCore+ConnectionStatus.m index a0e87a6b..1cdaf850 100644 --- a/ownCloudSDK/Core/Connection Status/OCCore+ConnectionStatus.m +++ b/ownCloudSDK/Core/Connection Status/OCCore+ConnectionStatus.m @@ -20,7 +20,7 @@ #import "OCCore+Internal.h" #import "OCLogger.h" #import "OCCore+SyncEngine.h" -#import "OCCoreMaintenanceModeStatusSignalProvider.h" +#import "OCCoreServerStatusSignalProvider.h" #import "OCMacros.h" @implementation OCCore (ConnectionStatus) @@ -302,8 +302,22 @@ - (void)connectionChangedState:(OCConnection *)connection _connectionStatusSignalProvider.state = (connection.state == OCConnectionStateConnected) ? OCCoreConnectionStatusSignalStateTrue : OCCoreConnectionStatusSignalStateFalse; } -- (OCConnectionRequestInstruction)connection:(OCConnection *)connection instructionForFinishedRequest:(OCConnectionRequest *)finishedRequest defaultsTo:(OCConnectionRequestInstruction)defaulInstruction +- (OCConnectionRequestInstruction)connection:(OCConnection *)connection instructionForFinishedRequest:(OCConnectionRequest *)finishedRequest error:(NSError *)error defaultsTo:(OCConnectionRequestInstruction)defaultInstruction { + if (error != nil) + { +// if ([error.domain isEqual:(__bridge NSString *)kCFErrorDomainCFNetwork] && (error.code == kCFURLErrorCannotConnectToHost)) + if ([error.domain isEqual:NSURLErrorDomain] && (error.code == NSURLErrorCannotConnectToHost)) + { + [_serverStatusSignalProvider reportConnectionRefusedError]; + + if ([finishedRequest.requiredSignals containsObject:OCConnectionSignalIDCoreOnline]) + { + return (OCConnectionRequestInstructionReschedule); + } + } + } + if (finishedRequest.responseHTTPStatus.code == OCHTTPStatusCodeSERVICE_UNAVAILABLE) { [self reportResponseIndicatingMaintenanceMode]; @@ -314,13 +328,13 @@ - (OCConnectionRequestInstruction)connection:(OCConnection *)connection instruct } } - return (defaulInstruction); + return (defaultInstruction); } #pragma mark - Reporting - (void)reportResponseIndicatingMaintenanceMode { - [_maintenanceModeStatusSignalProvider reportReponseIndicatingMaintenanceMode]; + [_serverStatusSignalProvider reportResponseIndicatingMaintenanceMode]; } @end diff --git a/ownCloudSDK/Core/Connection Status/Maintenance Mode/OCCoreMaintenanceModeStatusSignalProvider.h b/ownCloudSDK/Core/Connection Status/Server/OCCoreServerStatusSignalProvider.h similarity index 74% rename from ownCloudSDK/Core/Connection Status/Maintenance Mode/OCCoreMaintenanceModeStatusSignalProvider.h rename to ownCloudSDK/Core/Connection Status/Server/OCCoreServerStatusSignalProvider.h index 46fec154..cdc918c7 100644 --- a/ownCloudSDK/Core/Connection Status/Maintenance Mode/OCCoreMaintenanceModeStatusSignalProvider.h +++ b/ownCloudSDK/Core/Connection Status/Server/OCCoreServerStatusSignalProvider.h @@ -1,5 +1,5 @@ // -// OCCoreMaintenanceModeStatusSignalProvider.h +// OCCoreServerStatusSignalProvider.h // ownCloudSDK // // Created by Felix Schwarz on 06.12.18. @@ -20,12 +20,14 @@ NS_ASSUME_NONNULL_BEGIN -@interface OCCoreMaintenanceModeStatusSignalProvider : OCCoreConnectionStatusSignalProvider +@interface OCCoreServerStatusSignalProvider : OCCoreConnectionStatusSignalProvider { NSTimer *_statusPollTimer; } -- (void)reportReponseIndicatingMaintenanceMode; +- (void)reportResponseIndicatingMaintenanceMode; + +- (void)reportConnectionRefusedError; @end diff --git a/ownCloudSDK/Core/Connection Status/Maintenance Mode/OCCoreMaintenanceModeStatusSignalProvider.m b/ownCloudSDK/Core/Connection Status/Server/OCCoreServerStatusSignalProvider.m similarity index 81% rename from ownCloudSDK/Core/Connection Status/Maintenance Mode/OCCoreMaintenanceModeStatusSignalProvider.m rename to ownCloudSDK/Core/Connection Status/Server/OCCoreServerStatusSignalProvider.m index b97c2cdd..9cc1118d 100644 --- a/ownCloudSDK/Core/Connection Status/Maintenance Mode/OCCoreMaintenanceModeStatusSignalProvider.m +++ b/ownCloudSDK/Core/Connection Status/Server/OCCoreServerStatusSignalProvider.m @@ -1,5 +1,5 @@ // -// OCCoreMaintenanceModeStatusSignalProvider.m +// OCCoreServerStatusSignalProvider.m // ownCloudSDK // // Created by Felix Schwarz on 06.12.18. @@ -16,9 +16,10 @@ * */ -#import "OCCoreMaintenanceModeStatusSignalProvider.h" +#import "OCCoreServerStatusSignalProvider.h" +#import "OCMacros.h" -@implementation OCCoreMaintenanceModeStatusSignalProvider +@implementation OCCoreServerStatusSignalProvider - (instancetype)init { @@ -62,6 +63,7 @@ - (void)_sendStatusPollRequest:(NSTimer *)timer { if (!maintenanceMode.boolValue) { + self.shortDescription = nil; self.state = OCCoreConnectionStatusSignalStateTrue; [self setStatusPollTimerActive:NO]; } @@ -70,7 +72,7 @@ - (void)_sendStatusPollRequest:(NSTimer *)timer }]; } -- (void)reportReponseIndicatingMaintenanceMode +- (void)reportResponseIndicatingMaintenanceMode { @synchronized(self) { @@ -80,4 +82,15 @@ - (void)reportReponseIndicatingMaintenanceMode } } +- (void)reportConnectionRefusedError +{ + @synchronized(self) + { + self.state = OCCoreConnectionStatusSignalStateFalse; + self.shortDescription = OCLocalized(@"Connection refused"); + + [self setStatusPollTimerActive:YES]; + } +} + @end diff --git a/ownCloudSDK/Core/FileProvider/OCCore+FileProvider.h b/ownCloudSDK/Core/FileProvider/OCCore+FileProvider.h index 43da1f6c..0ed942a4 100644 --- a/ownCloudSDK/Core/FileProvider/OCCore+FileProvider.h +++ b/ownCloudSDK/Core/FileProvider/OCCore+FileProvider.h @@ -25,14 +25,14 @@ NS_ASSUME_NONNULL_BEGIN @property(class,nonatomic,readonly) BOOL hostHasFileProvider; #pragma mark - Fileprovider tools -- (void)retrieveItemFromDatabaseForFileID:(OCFileID)fileID completionHandler:(void(^)(NSError * __nullable error, OCSyncAnchor __nullable syncAnchor, OCItem * __nullable itemFromDatabase))completionHandler; +- (void)retrieveItemFromDatabaseForLocalID:(OCLocalID)localID completionHandler:(void(^)(NSError * __nullable error, OCSyncAnchor __nullable syncAnchor, OCItem * __nullable itemFromDatabase))completionHandler; #pragma mark - File provider manager - (nullable NSFileProviderManager *)fileProviderManager; #pragma mark - Signal changes for items - (void)signalChangesForItems:(NSArray *)changedItems; -- (void)signalEnumeratorForContainerItemIdentifier:(NSFileProviderItemIdentifier)changedDirectoryFileID; +- (void)signalEnumeratorForContainerItemIdentifier:(NSFileProviderItemIdentifier)changedDirectoryLocalID; @end diff --git a/ownCloudSDK/Core/FileProvider/OCCore+FileProvider.m b/ownCloudSDK/Core/FileProvider/OCCore+FileProvider.m index d68598e4..c11e0232 100644 --- a/ownCloudSDK/Core/FileProvider/OCCore+FileProvider.m +++ b/ownCloudSDK/Core/FileProvider/OCCore+FileProvider.m @@ -46,11 +46,11 @@ + (BOOL)hostHasFileProvider } #pragma mark - Fileprovider tools -- (void)retrieveItemFromDatabaseForFileID:(OCFileID)fileID completionHandler:(void(^)(NSError * __nullable error, OCSyncAnchor __nullable syncAnchor, OCItem * __nullable itemFromDatabase))completionHandler +- (void)retrieveItemFromDatabaseForLocalID:(OCLocalID)localID completionHandler:(void(^)(NSError * __nullable error, OCSyncAnchor __nullable syncAnchor, OCItem * __nullable itemFromDatabase))completionHandler { [self queueBlock:^{ OCSyncExec(cacheItemRetrieval, { - [self.vault.database retrieveCacheItemForFileID:fileID completionHandler:^(OCDatabase *db, NSError *error, OCSyncAnchor syncAnchor, OCItem *item) { + [self.vault.database retrieveCacheItemForLocalID:localID completionHandler:^(OCDatabase *db, NSError *error, OCSyncAnchor syncAnchor, OCItem *item) { completionHandler(error, syncAnchor, item); OCSyncExecDone(cacheItemRetrieval); @@ -59,6 +59,7 @@ - (void)retrieveItemFromDatabaseForFileID:(OCFileID)fileID completionHandler:(vo }]; } + #pragma mark - File provider manager - (NSFileProviderManager *)fileProviderManager { @@ -86,8 +87,8 @@ - (void)signalChangesForItems:(NSArray *)changedItems { if (self.postFileProviderNotifications) { - NSMutableSet *changedDirectoriesFileIDs = [NSMutableSet new]; - OCFileID rootDirectoryFileID = nil; + NSMutableSet *changedDirectoriesLocalIDs = [NSMutableSet new]; + OCLocalID rootDirectoryLocalID = nil; BOOL addRoot = NO; // Coalesce IDs @@ -96,9 +97,9 @@ - (void)signalChangesForItems:(NSArray *)changedItems switch (item.type) { case OCItemTypeFile: - if (item.parentFileID != nil) + if (item.parentLocalID != nil) { - [changedDirectoriesFileIDs addObject:item.parentFileID]; + [changedDirectoriesLocalIDs addObject:item.parentLocalID]; } else if ([item.path.parentPath isEqual:@"/"]) { @@ -109,45 +110,45 @@ - (void)signalChangesForItems:(NSArray *)changedItems case OCItemTypeCollection: if ([item.path isEqual:@"/"]) { - rootDirectoryFileID = item.fileID; + rootDirectoryLocalID = item.localID; addRoot = YES; } else { - if (item.parentFileID != nil) + if (item.parentLocalID != nil) { - [changedDirectoriesFileIDs addObject:item.parentFileID]; + [changedDirectoriesLocalIDs addObject:item.parentLocalID]; } - if (item.fileID != nil) + if (item.localID != nil) { - [changedDirectoriesFileIDs addObject:item.fileID]; + [changedDirectoriesLocalIDs addObject:item.localID]; } } break; } - if ((rootDirectoryFileID==nil) && [item.path.parentPath isEqual:@"/"] && (item.parentFileID!=nil)) + if ((rootDirectoryLocalID==nil) && [item.path.parentPath isEqual:@"/"] && (item.parentLocalID!=nil)) { - rootDirectoryFileID = item.parentFileID; + rootDirectoryLocalID = item.parentLocalID; } } - // Remove root directory fileID - if (rootDirectoryFileID != nil) + // Remove root directory localID + if (rootDirectoryLocalID != nil) { - if ([changedDirectoriesFileIDs containsObject:rootDirectoryFileID]) + if ([changedDirectoriesLocalIDs containsObject:rootDirectoryLocalID]) { - [changedDirectoriesFileIDs removeObject:rootDirectoryFileID]; + [changedDirectoriesLocalIDs removeObject:rootDirectoryLocalID]; addRoot = YES; } } - // Add root directory fileID + // Add root directory localID if (addRoot) { - [changedDirectoriesFileIDs addObject:NSFileProviderRootContainerItemIdentifier]; + [changedDirectoriesLocalIDs addObject:NSFileProviderRootContainerItemIdentifier]; } // Signal NSFileProviderManager @@ -156,43 +157,43 @@ - (void)signalChangesForItems:(NSArray *)changedItems dispatch_async(dispatch_get_main_queue(), ^{ NSFileProviderManager *fileProviderManager = [NSFileProviderManager managerForDomain:self->_vault.fileProviderDomain]; - for (OCFileID changedDirectoryFileID in changedDirectoriesFileIDs) + for (OCLocalID changedDirectoryLocalID in changedDirectoriesLocalIDs) { - OCLogDebug(@"Signaling changes to file provider manager %@ for item file ID %@", fileProviderManager, OCLogPrivate(changedDirectoryFileID)); + OCLogDebug(@"Signaling changes to file provider manager %@ for item localID=%@", fileProviderManager, OCLogPrivate(changedDirectoryLocalID)); - [self signalEnumeratorForContainerItemIdentifier:changedDirectoryFileID]; + [self signalEnumeratorForContainerItemIdentifier:changedDirectoryLocalID]; } }); } } } -- (void)signalEnumeratorForContainerItemIdentifier:(NSFileProviderItemIdentifier)changedDirectoryFileID +- (void)signalEnumeratorForContainerItemIdentifier:(NSFileProviderItemIdentifier)changedDirectoryLocalID { @synchronized(_fileProviderSignalCountByContainerItemIdentifiersLock) { - NSNumber *currentSignalCount = _fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryFileID]; + NSNumber *currentSignalCount = _fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryLocalID]; if (currentSignalCount == nil) { // The only/first signal for this right now => schedule right away - _fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryFileID] = @(1); + _fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryLocalID] = @(1); - [self _scheduleSignalForContainerItemIdentifier:changedDirectoryFileID]; + [self _scheduleSignalForContainerItemIdentifier:changedDirectoryLocalID]; } else { // Another signal hasn't completed yet, so increase the counter and wait for the scheduled signal to complete // (at which point, another signal will be triggered) - OCLogDebug(@"Skipped signaling %@ for changes as another signal hasn't completed yet", changedDirectoryFileID); + OCLogDebug(@"Skipped signaling %@ for changes as another signal hasn't completed yet", changedDirectoryLocalID); - _fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryFileID] = @(_fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryFileID].integerValue + 1); + _fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryLocalID] = @(_fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryLocalID].integerValue + 1); } } } -- (void)_scheduleSignalForContainerItemIdentifier:(NSFileProviderItemIdentifier)changedDirectoryFileID +- (void)_scheduleSignalForContainerItemIdentifier:(NSFileProviderItemIdentifier)changedDirectoryLocalID { NSTimeInterval minimumSignalInterval = 0.2; // effectively throttle FP container update notifications to at most once per [minimumSignalInterval] @@ -203,30 +204,30 @@ - (void)_scheduleSignalForContainerItemIdentifier:(NSFileProviderItemIdentifier) { @synchronized(self->_fileProviderSignalCountByContainerItemIdentifiersLock) { - NSInteger signalCountAtStart = self->_fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryFileID].integerValue; + NSInteger signalCountAtStart = self->_fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryLocalID].integerValue; - OCLogDebug(@"Signaling %@ for changes..", changedDirectoryFileID); + OCLogDebug(@"Signaling %@ for changes..", changedDirectoryLocalID); - [fileProviderManager signalEnumeratorForContainerItemIdentifier:changedDirectoryFileID completionHandler:^(NSError * _Nullable error) { - OCLogDebug(@"Signaling %@ for changes ended with error %@", changedDirectoryFileID, error); + [fileProviderManager signalEnumeratorForContainerItemIdentifier:changedDirectoryLocalID completionHandler:^(NSError * _Nullable error) { + OCLogDebug(@"Signaling %@ for changes ended with error %@", changedDirectoryLocalID, error); dispatch_async(dispatch_get_main_queue(), ^{ @synchronized(self->_fileProviderSignalCountByContainerItemIdentifiersLock) { - NSInteger signalCountAtEnd = self->_fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryFileID].integerValue; + NSInteger signalCountAtEnd = self->_fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryLocalID].integerValue; NSInteger remainingSignalCount = signalCountAtEnd - signalCountAtStart; if (remainingSignalCount > 0) { // There were signals after initiating the last signal => schedule another signal - self->_fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryFileID] = @(remainingSignalCount); + self->_fileProviderSignalCountByContainerItemIdentifiers[changedDirectoryLocalID] = @(remainingSignalCount); - [self _scheduleSignalForContainerItemIdentifier:changedDirectoryFileID]; + [self _scheduleSignalForContainerItemIdentifier:changedDirectoryLocalID]; } else { // The last signal was sent after the last signal was requested => remove from dict - [self->_fileProviderSignalCountByContainerItemIdentifiers removeObjectForKey:changedDirectoryFileID]; + [self->_fileProviderSignalCountByContainerItemIdentifiers removeObjectForKey:changedDirectoryLocalID]; } } }); @@ -235,7 +236,7 @@ - (void)_scheduleSignalForContainerItemIdentifier:(NSFileProviderItemIdentifier) } else { - OCLogDebug(@"Signaling %@ for changes failed because the file provider manager couldn't be found.", changedDirectoryFileID); + OCLogDebug(@"Signaling %@ for changes failed because the file provider manager couldn't be found.", changedDirectoryLocalID); } }); diff --git a/ownCloudSDK/Core/FileProvider/OCSyncAction+FileProvider.m b/ownCloudSDK/Core/FileProvider/OCSyncAction+FileProvider.m index f9655881..4a22c49b 100644 --- a/ownCloudSDK/Core/FileProvider/OCSyncAction+FileProvider.m +++ b/ownCloudSDK/Core/FileProvider/OCSyncAction+FileProvider.m @@ -46,12 +46,12 @@ - (void)setupProgressSupportForItem:(OCItem *)item options:(NSDictionary **)opti OCConnectionRequestObserver observer = [^(OCConnectionRequest *request, OCConnectionRequestObserverEvent event) { if (event == OCConnectionRequestObserverEventTaskResume) { - [[NSFileProviderManager managerForDomain:fileProviderDomain] registerURLSessionTask:request.urlSessionTask forItemWithIdentifier:item.fileID completionHandler:^(NSError * _Nullable error) { + [[NSFileProviderManager managerForDomain:fileProviderDomain] registerURLSessionTask:request.urlSessionTask forItemWithIdentifier:item.localID completionHandler:^(NSError * _Nullable error) { OCLogDebug(@"record %@ returned from registering URLTask %@ for %@ with error=%@", syncContext.syncRecord, request.urlSessionTask, item, error); if (error != nil) { - OCLogError(@"error registering %@ for %@: %@", request.urlSessionTask, item.fileID, error); + OCLogError(@"error registering %@ for %@: %@", request.urlSessionTask, item.localID, error); } // File provider detail: the task may not be started until after this completionHandler was called diff --git a/ownCloudSDK/Core/ItemList/OCCore+ItemList.m b/ownCloudSDK/Core/ItemList/OCCore+ItemList.m index 2db69f16..17ab7f2e 100644 --- a/ownCloudSDK/Core/ItemList/OCCore+ItemList.m +++ b/ownCloudSDK/Core/ItemList/OCCore+ItemList.m @@ -115,6 +115,8 @@ - (OCCoreItemListTask *)_scheduleItemListTaskForPath:(OCPath)path { _itemListTasksByPath[task.path] = task; + [self.activityManager update:[OCActivityUpdate publishingActivityFor:task]]; + // Start item list task if (task.syncAnchorAtStart == nil) { @@ -153,6 +155,8 @@ - (void)_finishedItemListTask:(OCCoreItemListTask *)finishedTask [self scheduleNextItemListTask]; } } + + [self.activityManager update:[OCActivityUpdate unpublishActivityFor:finishedTask]]; } } @@ -164,7 +168,6 @@ - (void)handleUpdatedTask:(OCCoreItemListTask *)task BOOL targetRemoved = NO; NSMutableArray *queryResults = nil; __block NSMutableArray *queryResultsChangedItems = nil; - OCItem *taskRootItem = nil; NSString *taskPath = task.path; __block OCSyncAnchor querySyncAnchor = nil; OCCoreItemListTask *nextTask = nil; @@ -266,8 +269,8 @@ - (void)handleUpdatedTask:(OCCoreItemListTask *)task // Perform merge OCCoreItemList *cacheSet = task.cachedSet; OCCoreItemList *retrievedSet = task.retrievedSet; - NSMutableDictionary *cacheItemsByFileID = cacheSet.itemsByFileID; - NSMutableDictionary *retrievedItemsByFileID = retrievedSet.itemsByFileID; + NSMutableDictionary *cacheItemsByFileID = cacheSet.itemsByFileID; + NSMutableDictionary *retrievedItemsByFileID = retrievedSet.itemsByFileID; NSMutableDictionary *cacheItemsByPath = cacheSet.itemsByPath; NSMutableDictionary *retrievedItemsByPath = retrievedSet.itemsByPath; @@ -345,6 +348,7 @@ - (void)handleUpdatedTask:(OCCoreItemListTask *)task ) { // Preserve local item, but merge in info on latest server version + retrievedItem.localID = cacheItem.localID; cacheItem.remoteItem = retrievedItem; // Return updated cached version @@ -416,6 +420,89 @@ - (void)handleUpdatedTask:(OCCoreItemListTask *)task } }]; + // Preserve localID for remotely moved, known items + { + NSMutableIndexSet *removeItemsFromDeletedItemsIndexes = nil; + NSMutableIndexSet *removeItemsFromNewItemsIndexes = nil; + + NSUInteger newItemIndex = 0; + + for (OCItem *newItem in newItems) + { + __block OCItem *knownItem = nil; + + [self.database retrieveCacheItemForFileID:newItem.fileID includingRemoved:YES completionHandler:^(OCDatabase *db, NSError *error, OCSyncAnchor syncAnchor, OCItem *item) { + knownItem = item; + knownItem.removed = NO; + }]; + + if (knownItem != nil) + { + NSUInteger index = 0; + + // Move over metaData + OCLocalID parentLocalID = newItem.parentLocalID; + + [newItem prepareToReplace:knownItem]; + newItem.parentLocalID = parentLocalID; // Make sure the new parent localID is used + + newItem.locallyModified = knownItem.locallyModified; // Keep metadata on local copy + newItem.localRelativePath = knownItem.localRelativePath; + newItem.localCopyVersionIdentifier = knownItem.localCopyVersionIdentifier; + + if (![knownItem.path isEqual:newItem.path]) + { + // If paths aren't identical => pass along metadata + newItem.previousPath = knownItem.path; + } + + // Remove from deletedCacheItems + for (OCItem *deletedItem in deletedCacheItems) + { + if ([deletedItem.databaseID isEqual:newItem.databaseID]) + { + if (removeItemsFromDeletedItemsIndexes == nil) + { + removeItemsFromDeletedItemsIndexes = [[NSMutableIndexSet alloc] initWithIndex:index]; + } + else + { + [removeItemsFromDeletedItemsIndexes addIndex:index]; + } + } + + index++; + } + + // Remove from newItems + if (removeItemsFromNewItemsIndexes == nil) + { + removeItemsFromNewItemsIndexes = [[NSMutableIndexSet alloc] initWithIndex:newItemIndex]; + } + else + { + [removeItemsFromNewItemsIndexes addIndex:newItemIndex]; + } + + // Add to updatedItems + [changedCacheItems addObject:newItem]; + } + + newItemIndex++; + } + + // Commit changes + if (removeItemsFromDeletedItemsIndexes != nil) + { + [deletedCacheItems removeObjectsAtIndexes:removeItemsFromDeletedItemsIndexes]; + } + + if (removeItemsFromNewItemsIndexes != nil) + { + [newItems removeObjectsAtIndexes:removeItemsFromNewItemsIndexes]; + } + } + // Export sync anchor value querySyncAnchor = newSyncAnchor; @@ -469,7 +556,7 @@ - (void)handleUpdatedTask:(OCCoreItemListTask *)task completionHandler(); } afterQueryUpdates:^(dispatch_block_t _Nonnull completionHandler) { - [self _finalizeQueryUpdatesWithQueryResults:queryResults queryResultsChangedItems:queryResultsChangedItems queryState:queryState querySyncAnchor:querySyncAnchor task:task taskPath:taskPath taskRootItem:taskRootItem targetRemoved:targetRemoved]; + [self _finalizeQueryUpdatesWithQueryResults:queryResults queryResultsChangedItems:queryResultsChangedItems queryState:queryState querySyncAnchor:querySyncAnchor task:task taskPath:taskPath targetRemoved:targetRemoved]; completionHandler(); } queryPostProcessor:nil @@ -552,7 +639,7 @@ - (void)handleUpdatedTask:(OCCoreItemListTask *)task [self queueBlock:^{ // Update non-idle queries - [self _finalizeQueryUpdatesWithQueryResults:queryResults queryResultsChangedItems:queryResultsChangedItems queryState:queryState querySyncAnchor:querySyncAnchor task:task taskPath:taskPath taskRootItem:taskRootItem targetRemoved:targetRemoved]; + [self _finalizeQueryUpdatesWithQueryResults:queryResults queryResultsChangedItems:queryResultsChangedItems queryState:queryState querySyncAnchor:querySyncAnchor task:task taskPath:taskPath targetRemoved:targetRemoved]; if (removeTask) { @@ -579,9 +666,11 @@ - (void)handleUpdatedTask:(OCCoreItemListTask *)task [self endActivity:@"item list task"]; } -- (void)_finalizeQueryUpdatesWithQueryResults:(NSMutableArray *)queryResults queryResultsChangedItems:(NSMutableArray *)queryResultsChangedItems queryState:(OCQueryState)queryState querySyncAnchor:(OCSyncAnchor)querySyncAnchor task:(OCCoreItemListTask * _Nonnull)task taskPath:(NSString *)taskPath taskRootItem:(OCItem *)taskRootItem targetRemoved:(BOOL)targetRemoved { +- (void)_finalizeQueryUpdatesWithQueryResults:(NSMutableArray *)queryResults queryResultsChangedItems:(NSMutableArray *)queryResultsChangedItems queryState:(OCQueryState)queryState querySyncAnchor:(OCSyncAnchor)querySyncAnchor task:(OCCoreItemListTask * _Nonnull)task taskPath:(NSString *)taskPath targetRemoved:(BOOL)targetRemoved +{ NSMutableDictionary *queryResultItemsByPath = nil; NSMutableArray *queryResultWithoutRootItem = nil; + OCItem *taskRootItem = nil; OCQueryState setQueryState = queryState; // NSString *parentTaskPath = [taskPath parentPath]; diff --git a/ownCloudSDK/Core/ItemList/OCCoreItemList.h b/ownCloudSDK/Core/ItemList/OCCoreItemList.h index 3c8e5c3a..92f66528 100644 --- a/ownCloudSDK/Core/ItemList/OCCoreItemList.h +++ b/ownCloudSDK/Core/ItemList/OCCoreItemList.h @@ -38,6 +38,9 @@ typedef NS_ENUM(NSUInteger, OCCoreItemListState) NSMutableDictionary *_itemsByFileID; NSSet *_itemFileIDsSet; + NSMutableDictionary *_itemsByLocalID; + NSSet *_itemLocalIDsSet; + NSMutableDictionary *> *_itemsByParentPaths; NSSet *_itemParentPaths; @@ -54,6 +57,9 @@ typedef NS_ENUM(NSUInteger, OCCoreItemListState) @property(readonly,strong,nonatomic) NSMutableDictionary *itemsByFileID; @property(readonly,strong,nonatomic) NSSet *itemFileIDsSet; +@property(readonly,strong,nonatomic) NSMutableDictionary *itemsByLocalID; +@property(readonly,strong,nonatomic) NSSet *itemLocalIDsSet; + @property(readonly,strong,nonatomic) NSMutableDictionary *> *itemsByParentPaths; @property(readonly,strong,nonatomic) NSSet *itemParentPaths; diff --git a/ownCloudSDK/Core/ItemList/OCCoreItemList.m b/ownCloudSDK/Core/ItemList/OCCoreItemList.m index 389bae0f..e2735545 100644 --- a/ownCloudSDK/Core/ItemList/OCCoreItemList.m +++ b/ownCloudSDK/Core/ItemList/OCCoreItemList.m @@ -141,6 +141,43 @@ - (void)setItems:(NSArray *)items return (_itemFileIDsSet); } +- (NSMutableDictionary *)itemsByLocalID +{ + if (_itemsByLocalID == nil) + { + _itemsByLocalID = [NSMutableDictionary new]; + + for (OCItem *item in self.items) + { + if (item.localID != nil) + { + _itemsByLocalID[item.localID] = item; + } + } + } + + return (_itemsByLocalID); +} + +- (NSSet *)itemLocalIDsSet +{ + if (_itemLocalIDsSet == nil) + { + NSArray *itemLocalIDs; + + if ((itemLocalIDs = [self.itemsByLocalID allKeys]) != nil) + { + _itemLocalIDsSet = [[NSSet alloc] initWithArray:itemLocalIDs]; + } + else + { + _itemLocalIDsSet = [NSSet new]; + } + } + + return (_itemLocalIDsSet); +} + - (NSMutableDictionary *> *)itemsByParentPaths { if (_itemsByParentPaths == nil) diff --git a/ownCloudSDK/Core/ItemList/OCCoreItemListTask.h b/ownCloudSDK/Core/ItemList/OCCoreItemListTask.h index b457d50b..ae36a57f 100644 --- a/ownCloudSDK/Core/ItemList/OCCoreItemListTask.h +++ b/ownCloudSDK/Core/ItemList/OCCoreItemListTask.h @@ -20,6 +20,7 @@ #import "OCTypes.h" #import "OCItem.h" #import "OCCoreItemList.h" +#import "OCActivity.h" @class OCCore; @class OCCoreItemListTask; @@ -32,7 +33,7 @@ typedef NS_ENUM(NSUInteger, OCCoreTaskMergeStatus) typedef void(^OCCoreItemListTaskChangeHandler)(OCCore *core, OCCoreItemListTask *task); -@interface OCCoreItemListTask : NSObject +@interface OCCoreItemListTask : NSObject @property(weak) OCCore *core; @property(strong) OCPath path; diff --git a/ownCloudSDK/Core/ItemList/OCCoreItemListTask.m b/ownCloudSDK/Core/ItemList/OCCoreItemListTask.m index 7d3efaeb..19d69770 100644 --- a/ownCloudSDK/Core/ItemList/OCCoreItemListTask.m +++ b/ownCloudSDK/Core/ItemList/OCCoreItemListTask.m @@ -26,6 +26,14 @@ #import "OCCore+ConnectionStatus.h" #import "OCCore+ItemList.h" #import "OCMacros.h" +#import "NSProgress+OCExtensions.h" + +@interface OCCoreItemListTask () +{ + OCActivityIdentifier _activityIdentifier; +} + +@end @implementation OCCoreItemListTask @@ -136,7 +144,9 @@ - (void)_updateRetrievedSet void (^RetrieveItems)(OCItem *parentDirectoryItem) = ^(OCItem *parentDirectoryItem){ [self->_core queueConnectivityBlock:^{ [self->_core queueRequestJob:^(dispatch_block_t completionHandler) { - [self->_core.connection retrieveItemListAtPath:self.path depth:1 completionHandler:^(NSError *error, NSArray *items) { + NSProgress *retrievalProgress; + + retrievalProgress = [self->_core.connection retrieveItemListAtPath:self.path depth:1 completionHandler:^(NSError *error, NSArray *items) { [self->_core queueBlock:^{ // Update inside the core's serial queue to make sure we never change the data while the core is also working on it OCSyncAnchor latestSyncAnchor = [self.core retrieveLatestSyncAnchorWithError:NULL]; @@ -170,9 +180,20 @@ - (void)_updateRetrievedSet if (self.path != nil) { OCItem *rootItem; + OCItem *cachedRootItem; if ((rootItem = self->_retrievedSet.itemsByPath[self.path]) != nil) { + if ((cachedRootItem = self->_cachedSet.itemsByFileID[rootItem.fileID]) == nil) + { + cachedRootItem = self->_cachedSet.itemsByPath[self.path]; + } + + if (cachedRootItem != nil) + { + rootItem.localID = cachedRootItem.localID; + } + if ((rootItem.type == OCItemTypeCollection) && (items.count > 1)) { for (OCItem *item in items) @@ -180,6 +201,7 @@ - (void)_updateRetrievedSet if (item != rootItem) { item.parentFileID = rootItem.fileID; + item.parentLocalID = rootItem.localID; } } } @@ -188,6 +210,11 @@ - (void)_updateRetrievedSet { rootItem.parentFileID = parentDirectoryItem.fileID; } + + if (parentDirectoryItem.parentLocalID == nil) + { + rootItem.parentLocalID = parentDirectoryItem.localID; + } } } @@ -204,6 +231,11 @@ - (void)_updateRetrievedSet completionHandler(); }]; }]; + + if (retrievalProgress != nil) + { + [self.core.activityManager update:[[OCActivityUpdate updatingActivityFor:self] withProgress:retrievalProgress]]; + } }]; }]; }; @@ -282,4 +314,24 @@ - (void)_update [_core endActivity:@"update unstarted sets"]; } +#pragma mark - Activity source +- (OCActivityIdentifier)activityIdentifier +{ + if (_activityIdentifier == nil) + { + _activityIdentifier = [@"ItemListTask:" stringByAppendingString:NSUUID.UUID.UUIDString]; + } + + return (_activityIdentifier); +} + +- (OCActivity *)provideActivity +{ + OCActivity *activity = [OCActivity withIdentifier:self.activityIdentifier description:[NSString stringWithFormat:@"Retrieving items for %@", self.path] statusMessage:nil ranking:0]; + + activity.progress = NSProgress.indeterminateProgress; + + return (activity); +} + @end diff --git a/ownCloudSDK/Core/OCCore+ItemUpdates.m b/ownCloudSDK/Core/OCCore+ItemUpdates.m index 0494eb55..3d237ebb 100644 --- a/ownCloudSDK/Core/OCCore+ItemUpdates.m +++ b/ownCloudSDK/Core/OCCore+ItemUpdates.m @@ -345,11 +345,12 @@ - (void)performUpdatesForAddedItems:(nullable NSArray *)addedItems { OCItem *updatedRootItem = nil; - if (updatedItemList.itemsByParentPaths[query.queryPath].count > 0) + GetUpdatedFullResultsReady(); + + if ((updatedItemList.itemsByParentPaths[query.queryPath].count > 0) || // path match + ([updatedItemList.itemLocalIDsSet intersectsSet:updatedFullQueryResultsItemList.itemLocalIDsSet])) // Contained localID match { // Items were updated - GetUpdatedFullResultsReady(); - for (OCItem *item in updatedItemList.itemsByParentPaths[query.queryPath]) { if (!query.includeRootItem && [item.path isEqual:query.queryPath]) @@ -360,14 +361,19 @@ - (void)performUpdatesForAddedItems:(nullable NSArray *)addedItems if (item.path != nil) { - OCItem *removeItem; + OCItem *reMoveItem = nil; - if ((removeItem = updatedFullQueryResultsItemList.itemsByFileID[item.fileID]) != nil) + if ((reMoveItem = updatedFullQueryResultsItemList.itemsByFileID[item.fileID]) == nil) + { + reMoveItem = updatedFullQueryResultsItemList.itemsByLocalID[item.localID]; + } + + if (reMoveItem != nil) { NSUInteger replaceAtIndex; // Replace if found - if ((replaceAtIndex = [updatedFullQueryResults indexOfObjectIdenticalTo:removeItem]) != NSNotFound) + if ((replaceAtIndex = [updatedFullQueryResults indexOfObjectIdenticalTo:reMoveItem]) != NSNotFound) { [updatedFullQueryResults removeObjectAtIndex:replaceAtIndex]; [updatedFullQueryResults insertObject:item atIndex:replaceAtIndex]; @@ -418,33 +424,47 @@ - (void)performUpdatesForAddedItems:(nullable NSArray *)addedItems if (query.queryItem != nil) { // Only update queries that have already gone through their complete, initial content update - if (query.state == OCQueryStateIdle) + if ((query.state == OCQueryStateIdle) || + (query.state == OCQueryStateTargetRemoved)) // An item could appear removed temporarily when it was moved on the server and the item has not yet been seen by the core in its new location { OCPath queryItemPath = query.queryItem.path; - OCItem *newQueryItem = nil; + OCLocalID queryItemLocalID = query.queryItem.localID; + OCItem *resultItem = nil; if (addedItemList!=nil) { - if ((newQueryItem = addedItemList.itemsByPath[queryItemPath]) != nil) + if ((resultItem = addedItemList.itemsByPath[queryItemPath]) != nil) + { + query.state = OCQueryStateIdle; + query.fullQueryResults = [NSMutableArray arrayWithObject:resultItem]; + } + else if ((resultItem = addedItemList.itemsByLocalID[queryItemLocalID]) != nil) { - query.fullQueryResults = [NSMutableArray arrayWithObject:newQueryItem]; + query.state = OCQueryStateIdle; + query.fullQueryResults = [NSMutableArray arrayWithObject:resultItem]; } } if (updatedItemList!=nil) { - if ((newQueryItem = updatedItemList.itemsByPath[queryItemPath]) != nil) + if ((resultItem = updatedItemList.itemsByPath[queryItemPath]) != nil) { - query.fullQueryResults = [NSMutableArray arrayWithObject:newQueryItem]; + query.state = OCQueryStateIdle; + query.fullQueryResults = [NSMutableArray arrayWithObject:resultItem]; + } + else if ((resultItem = updatedItemList.itemsByLocalID[queryItemLocalID]) != nil) + { + query.state = OCQueryStateIdle; + query.fullQueryResults = [NSMutableArray arrayWithObject:resultItem]; } } if (removedItemList!=nil) { - if ((newQueryItem = updatedItemList.itemsByPath[queryItemPath]) != nil) + if ((removedItemList.itemsByPath[queryItemPath] != nil) || (removedItemList.itemsByLocalID[queryItemLocalID] != nil)) { - query.fullQueryResults = [NSMutableArray new]; query.state = OCQueryStateTargetRemoved; + query.fullQueryResults = [NSMutableArray new]; } } } diff --git a/ownCloudSDK/Core/OCCore.h b/ownCloudSDK/Core/OCCore.h index 34ae8284..e36dd9a8 100644 --- a/ownCloudSDK/Core/OCCore.h +++ b/ownCloudSDK/Core/OCCore.h @@ -29,6 +29,8 @@ #import "OCIPNotificationCenter.h" #import "OCLogTag.h" #import "OCAsyncSequentialQueue.h" +#import "OCActivityManager.h" +#import "OCActivityUpdate.h" @class OCCore; @class OCItem; @@ -37,7 +39,7 @@ @class OCIPNotificationCenter; @class OCCoreConnectionStatusSignalProvider; -@class OCCoreMaintenanceModeStatusSignalProvider; +@class OCCoreServerStatusSignalProvider; #pragma mark - Types typedef NS_ENUM(NSUInteger, OCCoreState) @@ -88,6 +90,8 @@ typedef void(^OCCorePlaceholderCompletionHandler)(NSError *error, OCItem *item); typedef void(^OCCoreCompletionHandler)(NSError *error); typedef void(^OCCoreStateChangedHandler)(OCCore *core); +typedef NSError *(^OCCoreImportTransformation)(NSURL *sourceURL); + typedef NSString* OCCoreOption NS_TYPED_ENUM; #pragma mark - Delegate @@ -124,9 +128,11 @@ NS_ASSUME_NONNULL_BEGIN NSMutableArray *_connectionStatusSignalProviders; OCCoreConnectionStatusSignalProvider *_reachabilityStatusSignalProvider; // Wrapping OCReachabilityMonitor or nw_path_monitor - OCCoreMaintenanceModeStatusSignalProvider *_maintenanceModeStatusSignalProvider; // Processes reports of maintenance mode repsonses and performs status.php polls for status changes + OCCoreServerStatusSignalProvider *_serverStatusSignalProvider; // Processes reports of connection refused and maintenance mode responses and performs status.php polls to detect the resolution of the issue OCCoreConnectionStatusSignalProvider *_connectionStatusSignalProvider; // Glue to include the OCConnection state into connection status (signal) + OCActivityManager *_activityManager; + OCEventHandlerIdentifier _eventHandlerIdentifier; BOOL _needsToProcessSyncRecords; @@ -157,7 +163,7 @@ NS_ASSUME_NONNULL_BEGIN BOOL _automaticItemListUpdatesEnabled; NSDate *_lastScheduledItemListUpdateDate; - NSMutableDictionary *> *_progressByFileID; + NSMutableDictionary *> *_progressByLocalID; __weak id _delegate; } @@ -176,6 +182,8 @@ NS_ASSUME_NONNULL_BEGIN @property(readonly,nonatomic) OCCoreConnectionStatusSignal connectionStatusSignals; //!< Mask of current connection status signals @property(readonly,strong,nullable) NSString *connectionStatusShortDescription; //!< Short description of the current connection status. +@property(readonly,strong) OCActivityManager *activityManager; + @property(readonly,strong) OCEventHandlerIdentifier eventHandlerIdentifier; @property(weak) id delegate; @@ -283,4 +291,9 @@ extern OCDatabaseCounterIdentifier OCCoreSyncJournalCounter; extern OCConnectionSignalID OCConnectionSignalIDCoreOnline; extern OCCoreOption OCCoreOptionImportByCopying; //!< [BOOL] Determines whether -[OCCore importFileNamed:..] should make a copy of the provided file, or move it (default). +extern OCCoreOption OCCoreOptionImportTransformation; //!< [OCCoreImportTransformation] Transformation to be applied on local item before upload extern OCCoreOption OCCoreOptionReturnImmediatelyIfOfflineOrUnavailable; //!< [BOOL] Determines whether -[OCCore downloadItem:..] should return immediately if the core is currently offline or unavailable. + +extern NSNotificationName OCCoreItemBeginsHavingProgress; //!< Notification sent when an item starts having progress. The object is the localID of the item. +extern NSNotificationName OCCoreItemChangedProgress; //!< Notification sent when an item's progress changed. The object is the localID of the item. +extern NSNotificationName OCCoreItemStopsHavingProgress; //!< Notification sent when an item no longer has any progress. The object is the localID of the item. diff --git a/ownCloudSDK/Core/OCCore.m b/ownCloudSDK/Core/OCCore.m index 8467ee02..fbb4ed60 100644 --- a/ownCloudSDK/Core/OCCore.m +++ b/ownCloudSDK/Core/OCCore.m @@ -35,7 +35,7 @@ #import "OCIPNotificationCenter.h" #import "OCCoreReachabilityConnectionStatusSignalProvider.h" #import "OCCoreNetworkPathMonitorSignalProvider.h" -#import "OCCoreMaintenanceModeStatusSignalProvider.h" +#import "OCCoreServerStatusSignalProvider.h" #import "OCCore+ConnectionStatus.h" #import "OCCore+Thumbnails.h" #import "OCCore+ItemUpdates.h" @@ -66,6 +66,8 @@ @implementation OCCore @synthesize connectionStatusSignals = _connectionStatusSignals; @synthesize connectionStatusShortDescription = _connectionStatusShortDescription; +@synthesize activityManager = _activityManager; + @synthesize eventHandlerIdentifier = _eventHandlerIdentifier; @synthesize latestSyncAnchor = _latestSyncAnchor; @@ -144,7 +146,9 @@ - (instancetype)initWithBookmark:(OCBookmark *)bookmark } }; - _progressByFileID = [NSMutableDictionary new]; + _progressByLocalID = [NSMutableDictionary new]; + + _activityManager = [[OCActivityManager alloc] initWithUpdateNotificationName:[@"OCCore.ActivityUpdate." stringByAppendingString:_bookmark.uuid.UUIDString]]; _thumbnailCache = [OCCache new]; @@ -170,11 +174,11 @@ - (instancetype)initWithBookmark:(OCBookmark *)bookmark { _reachabilityStatusSignalProvider = [[OCCoreReachabilityConnectionStatusSignalProvider alloc] initWithHostname:self.bookmark.url.host]; } - _maintenanceModeStatusSignalProvider = [OCCoreMaintenanceModeStatusSignalProvider new]; + _serverStatusSignalProvider = [OCCoreServerStatusSignalProvider new]; _connectionStatusSignalProvider = [[OCCoreConnectionStatusSignalProvider alloc] initWithSignal:OCCoreConnectionStatusSignalConnected initialState:OCCoreConnectionStatusSignalStateFalse stateProvider:nil]; [self addSignalProvider:_reachabilityStatusSignalProvider]; - [self addSignalProvider:_maintenanceModeStatusSignalProvider]; + [self addSignalProvider:_serverStatusSignalProvider]; [self addSignalProvider:_connectionStatusSignalProvider]; self.memoryConfiguration = OCCoreManager.sharedCoreManager.memoryConfiguration; @@ -308,6 +312,9 @@ - (void)stopWithCompletionHandler:(nullable OCCompletionHandler)completionHandle // Shut down Sync Engine [weakSelf shutdownSyncEngine]; + // Shut down progress + [weakSelf _shutdownProgressObservation]; + // Close connection OCCore *strongSelf; if ((strongSelf = weakSelf) != nil) @@ -711,68 +718,117 @@ - (nullable NSProgress *)terminateAvailableOfflineCapabilityForItem:(OCItem *)it #pragma mark - Progress tracking - (void)registerProgress:(NSProgress *)progress forItem:(OCItem *)item { - OCFileID fileID; + OCLocalID localID; + BOOL startsHavingProgress = NO; - if ((fileID = item.fileID) != nil) + if ((localID = item.localID) != nil) { - @synchronized(_progressByFileID) + @synchronized(_progressByLocalID) { NSMutableArray *progressObjects; - if ((progressObjects = _progressByFileID[fileID]) == nil) + if ((progressObjects = _progressByLocalID[localID]) == nil) { - progressObjects = (_progressByFileID[fileID] = [NSMutableArray new]); + progressObjects = (_progressByLocalID[localID] = [NSMutableArray new]); + startsHavingProgress = YES; } - [progressObjects addObject:progress]; + if ([progressObjects indexOfObjectIdenticalTo:progress] == NSNotFound) + { + [progressObjects addObject:progress]; + + progress.localID = localID; - [progress addObserver:self forKeyPath:@"isFinished" options:NSKeyValueObservingOptionInitial context:(__bridge void *)_progressByFileID]; - [progress addObserver:self forKeyPath:@"isCancelled" options:0 context:(__bridge void *)_progressByFileID]; - progress.fileID = fileID; + [progress addObserver:self forKeyPath:@"finished" options:NSKeyValueObservingOptionInitial context:(__bridge void *)_progressByLocalID]; + [progress addObserver:self forKeyPath:@"cancelled" options:0 context:(__bridge void *)_progressByLocalID]; + } } } + + if (startsHavingProgress) + { + [[NSNotificationCenter defaultCenter] postNotificationName:OCCoreItemBeginsHavingProgress object:item.localID]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:OCCoreItemChangedProgress object:item.localID]; } - (void)unregisterProgress:(NSProgress *)progress forItem:(OCItem *)item { - OCFileID fileID; + OCLocalID localID; - if ((fileID = item.fileID) != nil) + if ((localID = item.localID) != nil) { - [self unregisterProgress:progress forFileID:fileID]; + [self unregisterProgress:progress forLocalID:localID]; } } -- (void)unregisterProgress:(NSProgress *)progress forFileID:(OCFileID)fileID +- (void)unregisterProgress:(NSProgress *)progress forLocalID:(OCLocalID)localID { - if (fileID != nil) + BOOL stopsHavingProgress = NO; + + if (localID != nil) { - @synchronized(_progressByFileID) + @synchronized(_progressByLocalID) { NSMutableArray *progressObjects; - if ((progressObjects = _progressByFileID[fileID]) != nil) + if ((progressObjects = _progressByLocalID[localID]) != nil) { - [progressObjects removeObjectIdenticalTo:progress]; + if ([progressObjects indexOfObjectIdenticalTo:progress] != NSNotFound) + { + [progress removeObserver:self forKeyPath:@"finished" context:(__bridge void *)_progressByLocalID]; + [progress removeObserver:self forKeyPath:@"cancelled" context:(__bridge void *)_progressByLocalID]; - [progress removeObserver:self forKeyPath:@"isFinished" context:(__bridge void *)_progressByFileID]; - [progress removeObserver:self forKeyPath:@"isCancelled" context:(__bridge void *)_progressByFileID]; + [progressObjects removeObjectIdenticalTo:progress]; + + if (progressObjects.count == 0) + { + [_progressByLocalID removeObjectForKey:localID]; + stopsHavingProgress = YES; + } + } } } } + + [[NSNotificationCenter defaultCenter] postNotificationName:OCCoreItemChangedProgress object:localID]; + + if (stopsHavingProgress) + { + [[NSNotificationCenter defaultCenter] postNotificationName:OCCoreItemStopsHavingProgress object:localID]; + } +} + +- (void)_shutdownProgressObservation +{ + @synchronized(_progressByLocalID) + { + [_progressByLocalID enumerateKeysAndObjectsUsingBlock:^(OCFileID _Nonnull key, NSMutableArray * _Nonnull progressObjects, BOOL * _Nonnull stop) { + for (NSProgress *progress in progressObjects) + { + [progress removeObserver:self forKeyPath:@"finished" context:(__bridge void *)self->_progressByLocalID]; + [progress removeObserver:self forKeyPath:@"cancelled" context:(__bridge void *)self->_progressByLocalID]; + } + }]; + + [_progressByLocalID removeAllObjects]; + } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (context == (__bridge void *)_progressByFileID) + if (context == (__bridge void *)_progressByLocalID) { if ([object isKindOfClass:[NSProgress class]]) { NSProgress *progress = object; - if ((progress.isFinished || progress.isCancelled) && (progress.fileID != nil)) + if ((progress.isFinished || progress.isCancelled) && (progress.localID != nil)) { - [self unregisterProgress:progress forFileID:progress.fileID]; + [self queueBlock:^{ + [self unregisterProgress:progress forLocalID:progress.localID]; + }]; } } } @@ -786,15 +842,15 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N { NSMutableArray *resultProgressObjects = nil; - OCFileID fileID; + OCLocalID localID; - if ((fileID = item.fileID) != nil) + if ((localID = item.localID) != nil) { - @synchronized(_progressByFileID) + @synchronized(_progressByLocalID) { NSMutableArray *progressObjects; - if ((progressObjects = _progressByFileID[fileID]) != nil) + if ((progressObjects = _progressByLocalID[localID]) != nil) { if (eventType == OCEventTypeNone) { @@ -1138,4 +1194,9 @@ + (void)initialize OCConnectionSignalID OCConnectionSignalIDCoreOnline = @"coreOnline"; OCCoreOption OCCoreOptionImportByCopying = @"importByCopying"; +OCCoreOption OCCoreOptionImportTransformation = @"importTransformation"; OCCoreOption OCCoreOptionReturnImmediatelyIfOfflineOrUnavailable = @"returnImmediatelyIfOfflineOrUnavailable"; + +NSNotificationName OCCoreItemBeginsHavingProgress = @"OCCoreItemBeginsHavingProgress"; +NSNotificationName OCCoreItemChangedProgress = @"OCCoreItemChangedProgress"; +NSNotificationName OCCoreItemStopsHavingProgress = @"OCCoreItemStopsHavingProgress"; diff --git a/ownCloudSDK/Core/Sync/Actions/CopyMove/OCSyncActionCopyMove.m b/ownCloudSDK/Core/Sync/Actions/CopyMove/OCSyncActionCopyMove.m index dc87d760..6ae51665 100644 --- a/ownCloudSDK/Core/Sync/Actions/CopyMove/OCSyncActionCopyMove.m +++ b/ownCloudSDK/Core/Sync/Actions/CopyMove/OCSyncActionCopyMove.m @@ -63,6 +63,15 @@ - (void)preflightWithContext:(OCSyncContext *)syncContext OCItem *sourceItem = self.localItem; OCPath targetPath = [self.targetParentItem.path stringByAppendingPathComponent:self.targetName]; + if (sourceItem.type == OCItemTypeCollection) + { + // Ensure directory paths end with a slash + if (![targetPath hasSuffix:@"/"]) + { + targetPath = [targetPath stringByAppendingString:@"/"]; + } + } + if ([self.identifier isEqual:OCSyncActionIdentifierCopy]) { OCItem *placeholderItem = [OCItem placeholderItemOfType:sourceItem.type]; @@ -72,6 +81,7 @@ - (void)preflightWithContext:(OCSyncContext *)syncContext // Set path and parent folder placeholderItem.parentFileID = self.targetParentItem.fileID; + placeholderItem.parentLocalID = self.targetParentItem.localID; placeholderItem.path = targetPath; // Copy actual file if it exists locally @@ -118,9 +128,6 @@ - (void)preflightWithContext:(OCSyncContext *)syncContext // Add placeholder syncContext.addedItems = @[ placeholderItem ]; - - // Update item - syncContext.updatedItems = @[ sourceItem ]; } else if ([self.identifier isEqual:OCSyncActionIdentifierMove]) { @@ -136,6 +143,7 @@ - (void)preflightWithContext:(OCSyncContext *)syncContext updatedItem.previousPath = sourceItem.path; // Update location info + updatedItem.parentLocalID = self.targetParentItem.localID; updatedItem.parentFileID = self.targetParentItem.fileID; updatedItem.path = targetPath; @@ -238,8 +246,7 @@ - (OCCoreSyncInstruction)handleResultWithContext:(OCSyncContext *)syncContext [self.core renameDirectoryFromItem:sourceItem forItem:newItem adjustLocalMetadata:YES]; - syncContext.removedItems = @[ placeholderItem ]; - syncContext.addedItems = @[ newItem ]; + syncContext.updatedItems = @[ newItem ]; } else { @@ -251,10 +258,12 @@ - (OCCoreSyncInstruction)handleResultWithContext:(OCSyncContext *)syncContext else { OCItem *updatedItem = OCTypedCast(event.result, OCItem); + OCFileID updatedParentLocalID = updatedItem.parentLocalID; [sourceItem removeSyncRecordID:syncContext.syncRecord.recordID activity:OCItemSyncActivityUpdating]; [updatedItem prepareToReplace:self.localItem]; + updatedItem.parentLocalID = updatedParentLocalID; [self.core renameDirectoryFromItem:self.localItem forItem:updatedItem adjustLocalMetadata:YES]; diff --git a/ownCloudSDK/Core/Sync/Actions/CreateFolder/OCSyncActionCreateFolder.m b/ownCloudSDK/Core/Sync/Actions/CreateFolder/OCSyncActionCreateFolder.m index d882c49a..10ad6f5c 100644 --- a/ownCloudSDK/Core/Sync/Actions/CreateFolder/OCSyncActionCreateFolder.m +++ b/ownCloudSDK/Core/Sync/Actions/CreateFolder/OCSyncActionCreateFolder.m @@ -34,6 +34,7 @@ - (instancetype)initWithParentItem:(OCItem *)parentItem folderName:(NSString *)f if ((placeholderItem = [OCItem placeholderItemOfType:OCItemTypeCollection]) != nil) { placeholderItem.parentFileID = parentItem.fileID; + placeholderItem.parentLocalID = parentItem.localID; placeholderItem.path = [parentItem.path stringByAppendingPathComponent:folderName]; placeholderItem.lastModified = [NSDate date]; @@ -110,12 +111,17 @@ - (OCCoreSyncInstruction)handleResultWithContext:(OCSyncContext *)syncContext { [placeholderItem removeSyncRecordID:syncContext.syncRecord.recordID activity:OCItemSyncActivityCreating]; + newItem.previousPlaceholderFileID = placeholderItem.fileID; + newItem.parentFileID = placeholderItem.parentFileID; + + newItem.localID = placeholderItem.localID; + newItem.parentLocalID = placeholderItem.parentLocalID; + + placeholderItem.localID = nil; + syncContext.removedItems = @[ placeholderItem ]; } - newItem.previousPlaceholderFileID = placeholderItem.fileID; - newItem.parentFileID = placeholderItem.parentFileID; - syncContext.addedItems = @[ newItem ]; // Action complete and can be removed diff --git a/ownCloudSDK/Core/Sync/Actions/Download/OCCore+CommandDownload.m b/ownCloudSDK/Core/Sync/Actions/Download/OCCore+CommandDownload.m index 42ff1f45..68021507 100644 --- a/ownCloudSDK/Core/Sync/Actions/Download/OCCore+CommandDownload.m +++ b/ownCloudSDK/Core/Sync/Actions/Download/OCCore+CommandDownload.m @@ -24,7 +24,13 @@ @implementation OCCore (CommandDownload) #pragma mark - Command - (nullable NSProgress *)downloadItem:(OCItem *)item options:(nullable NSDictionary *)options resultHandler:(nullable OCCoreDownloadResultHandler)resultHandler { - return ([self _enqueueSyncRecordWithAction:[[OCSyncActionDownload alloc] initWithItem:item options:options] resultHandler:resultHandler]); + // Enqueue sync record + NSProgress *progress; + + progress = [self _enqueueSyncRecordWithAction:[[OCSyncActionDownload alloc] initWithItem:item options:options] resultHandler:resultHandler]; + progress.cancellable = YES; + + return (progress); } @end diff --git a/ownCloudSDK/Core/Sync/Actions/Download/OCSyncActionDownload.m b/ownCloudSDK/Core/Sync/Actions/Download/OCSyncActionDownload.m index dea0149b..1e8b298d 100644 --- a/ownCloudSDK/Core/Sync/Actions/Download/OCSyncActionDownload.m +++ b/ownCloudSDK/Core/Sync/Actions/Download/OCSyncActionDownload.m @@ -150,7 +150,7 @@ - (OCCoreSyncInstruction)scheduleWithContext:(OCSyncContext *)syncContext [syncContext.syncRecord addProgress:progress]; - [self.core registerProgress:progress forItem:item]; + [self.core registerProgress:syncContext.syncRecord.progress forItem:item]; } // Transition to processing diff --git a/ownCloudSDK/Core/Sync/Actions/OCSyncAction.h b/ownCloudSDK/Core/Sync/Actions/OCSyncAction.h index 7061b113..745ecd13 100644 --- a/ownCloudSDK/Core/Sync/Actions/OCSyncAction.h +++ b/ownCloudSDK/Core/Sync/Actions/OCSyncAction.h @@ -84,6 +84,10 @@ typedef NS_ENUM(NSUInteger, OCCoreSyncInstruction) #pragma mark - Cancellation handling - (OCCoreSyncInstruction)cancelWithContext:(OCSyncContext *)syncContext; //!< Called when the action is cancelled. Deschedules the record by default. +#pragma mark - Offline coalescation +- (NSError *)updatePreviousSyncRecord:(OCSyncRecord *)syncRecord context:(OCSyncContext *)syncContext; //!< If implemented, is called by the Sync Engine if there's a previous action in the queue targeting the same item. (TODO) +- (NSError *)updateActionWith:(OCItem *(^)(OCSyncContext *syncContext, OCSyncAction *syncAction, OCItem *item))actionUpdater context:(OCSyncContext *)syncContext; //!< Called when an action has not yet been scheduled and a subsequent action is queued on the same item. The actionUpdater block is provided by the subsequent action (TODO) + #pragma mark - Wait condition failure recovery - (BOOL)recoverFromWaitCondition:(OCWaitCondition *)waitCondition failedWithError:(NSError *)error context:(OCSyncContext *)syncContext; //!< Handles recovery from failed wait conditions. Returns YES if the Sync Engine should proceed processing (skipping removed/descheduled sync records, rerunning updated waitConditions and calling -scheduleWithContext: otherwise). diff --git a/ownCloudSDK/Core/Sync/Actions/OCSyncAction.m b/ownCloudSDK/Core/Sync/Actions/OCSyncAction.m index 38578a3a..90dd135d 100644 --- a/ownCloudSDK/Core/Sync/Actions/OCSyncAction.m +++ b/ownCloudSDK/Core/Sync/Actions/OCSyncAction.m @@ -138,6 +138,17 @@ - (OCCoreSyncInstruction)cancelWithContext:(OCSyncContext *)syncContext return (OCCoreSyncInstructionProcessNext); } +#pragma mark - Offline coalescation +- (NSError *)updatePreviousSyncRecord:(OCSyncRecord *)syncRecord context:(OCSyncContext *)syncContext +{ + return (OCError(OCErrorFeatureNotImplemented)); +} + +- (NSError *)updateActionWith:(OCItem *(^)(OCSyncContext *syncContext, OCSyncAction *syncAction, OCItem *item))actionUpdater context:(OCSyncContext *)syncContext +{ + return (OCError(OCErrorFeatureNotImplemented)); +} + #pragma mark - Wait condition failure handling - (BOOL)recoverFromWaitCondition:(OCWaitCondition *)waitCondition failedWithError:(NSError *)error context:(OCSyncContext *)syncContext { @@ -200,6 +211,8 @@ - (void)encodeWithCoder:(NSCoder *)coder [coder encodeObject:[self _archivedServerItemData] forKey:@"archivedServerItemData"]; [coder encodeObject:_parameters forKey:@"parameters"]; + [coder encodeObject:_localizedDescription forKey:@"localizedDescription"]; + [self encodeActionData:coder]; } @@ -214,6 +227,8 @@ - (instancetype)initWithCoder:(NSCoder *)decoder _parameters = [decoder decodeObjectOfClass:[NSDictionary class] forKey:@"parameters"]; + _localizedDescription = [decoder decodeObjectOfClass:[NSString class] forKey:@"localizedDescription"]; + [self decodeActionData:decoder]; } diff --git a/ownCloudSDK/Core/Sync/Actions/Upload/OCCore+CommandLocalImport.m b/ownCloudSDK/Core/Sync/Actions/Upload/OCCore+CommandLocalImport.m index 8f9b9f3d..b8036c21 100644 --- a/ownCloudSDK/Core/Sync/Actions/Upload/OCCore+CommandLocalImport.m +++ b/ownCloudSDK/Core/Sync/Actions/Upload/OCCore+CommandLocalImport.m @@ -45,6 +45,7 @@ - (nullable NSProgress *)importFileNamed:(nullable NSString *)newFileName at:(OC // Create placeholder item and fill fields required by -[NSFileProviderExtension importDocumentAtURL:toParentItemIdentifier:completionHandler:] completion handler placeholderItem = [OCItem placeholderItemOfType:OCItemTypeFile]; + placeholderItem.parentLocalID = parentItem.localID; placeholderItem.parentFileID = parentItem.fileID; placeholderItem.path = [parentItem.path stringByAppendingPathComponent:newFileName]; @@ -81,6 +82,31 @@ - (nullable NSProgress *)importFileNamed:(nullable NSString *)newFileName at:(OC importFileOperationSuccessful = [[NSFileManager defaultManager] moveItemAtURL:inputFileURL toURL:placeholderOutputURL error:&error]; } + if (importFileOperationSuccessful) + { + // Check for and apply transformations + OCCoreImportTransformation transformation; + + if ((transformation = options[OCCoreOptionImportTransformation]) != nil) + { + NSError *transformationError; + + OCLogDebug(@"Transforming transformation on item %@", OCLogPrivate(placeholderItem)); + + if ((transformationError = transformation(placeholderOutputURL)) == nil) + { + OCLogDebug(@"Transformation succeeded on item %@", OCLogPrivate(placeholderItem)); + [placeholderItem updateMetadataFromFileURL:placeholderOutputURL]; + } + else + { + OCLogDebug(@"Transformation failed with error=%@ on item %@", transformationError, OCLogPrivate(placeholderItem)); + error = transformationError; + importFileOperationSuccessful = NO; + } + } + } + if (importFileOperationSuccessful) { placeholderItem.localRelativePath = [self.vault relativePathForItem:placeholderItem]; @@ -129,7 +155,12 @@ - (nullable NSProgress *)importFileNamed:(nullable NSString *)newFileName at:(OC } // Enqueue sync record - return ([self _enqueueSyncRecordWithAction:[[OCSyncActionUpload alloc] initWithUploadItem:placeholderItem parentItem:parentItem filename:newFileName importFileURL:placeholderOutputURL isTemporaryCopy:NO] resultHandler:resultHandler]); + NSProgress *progress; + + progress = [self _enqueueSyncRecordWithAction:[[OCSyncActionUpload alloc] initWithUploadItem:placeholderItem parentItem:parentItem filename:newFileName importFileURL:placeholderOutputURL isTemporaryCopy:NO] resultHandler:resultHandler]; + progress.cancellable = YES; + + return (progress); } @end diff --git a/ownCloudSDK/Core/Sync/Actions/Upload/OCCore+CommandLocalModification.m b/ownCloudSDK/Core/Sync/Actions/Upload/OCCore+CommandLocalModification.m index c96d8709..372b4008 100644 --- a/ownCloudSDK/Core/Sync/Actions/Upload/OCCore+CommandLocalModification.m +++ b/ownCloudSDK/Core/Sync/Actions/Upload/OCCore+CommandLocalModification.m @@ -130,7 +130,12 @@ - (nullable NSProgress *)reportLocalModificationOfItem:(OCItem *)item parentItem } // Enqueue sync record - return ([self _enqueueSyncRecordWithAction:[[OCSyncActionUpload alloc] initWithUploadItem:item parentItem:parentItem filename:item.name importFileURL:temporaryFileURL isTemporaryCopy:YES] resultHandler:resultHandler]); + NSProgress *progress; + + progress = [self _enqueueSyncRecordWithAction:[[OCSyncActionUpload alloc] initWithUploadItem:item parentItem:parentItem filename:item.name importFileURL:temporaryFileURL isTemporaryCopy:YES] resultHandler:resultHandler]; + progress.cancellable = YES; + + return (progress); } @end diff --git a/ownCloudSDK/Core/Sync/Actions/Upload/OCSyncActionUpload.m b/ownCloudSDK/Core/Sync/Actions/Upload/OCSyncActionUpload.m index e56350e2..513bb170 100644 --- a/ownCloudSDK/Core/Sync/Actions/Upload/OCSyncActionUpload.m +++ b/ownCloudSDK/Core/Sync/Actions/Upload/OCSyncActionUpload.m @@ -170,7 +170,7 @@ - (OCCoreSyncInstruction)scheduleWithContext:(OCSyncContext *)syncContext { [syncContext.syncRecord addProgress:progress]; - [self.core registerProgress:progress forItem:self.localItem]; + [self.core registerProgress:syncContext.syncRecord.progress forItem:self.localItem]; } // Transition to processing @@ -193,119 +193,62 @@ - (OCCoreSyncInstruction)handleResultWithContext:(OCSyncContext *)syncContext if ((event.error == nil) && (event.result != nil)) { OCItem *uploadItem; + OCItem *uploadedItem = (OCItem *)event.result; if ((uploadItem = self.localItem) != nil) { - OCItem *uploadedItem = (OCItem *)event.result; - NSURL *uploadedItemURL = nil, *uploadItemURL = nil; + // Transfer localID + uploadedItem.localID = uploadItem.localID; + uploadedItem.parentLocalID = uploadItem.parentLocalID; + // Propagte previousPlaceholderFileID if (![uploadedItem.fileID isEqual:uploadItem.fileID]) { - // Uploaded item an upload item have different fileIDs (=> uploadItem could have been a placeholder) - - // Move file from uploadItem to uploadedItem backing storage - if (((uploadItemURL = [self.core.vault localURLForItem:uploadItem]) != nil) && - ((uploadedItemURL = [self.core.vault localURLForItem:uploadedItem]) != nil)) - { - NSError *error; - NSURL *placeholderItemContainerURL = [uploadItemURL URLByDeletingLastPathComponent]; - - // Create directory to house file for new item - if ((error = [self.core createDirectoryForItem:uploadedItem]) != nil) - { - OCLogError(@"Upload completion target directory creation failed for %@ with error %@", OCLogPrivate(uploadedItem), error); - } - - // Use _uploadCopyFileURL as source if available - if (_uploadCopyFileURL != nil) - { - uploadItemURL = _uploadCopyFileURL; - } - - // TODO: use new placeholder -> item transition API (-[OCCore renameDirectoryFromItem:forItem:adjustLocalMetadata:]) - - // Move file from placeholder to uploaded item URL - if ([[NSFileManager defaultManager] moveItemAtURL:uploadItemURL toURL:uploadedItemURL error:&error]) - { - // => File move successful - - // Update uploaded item with local relative path and remove the reference from placeholderItem - // - the locallyModified property is not mirrored to the uploadedItem as the file is now the same on the server - uploadedItem.localRelativePath = [self.core.vault relativePathForItem:uploadedItem]; - uploadItem.localRelativePath = nil; - - uploadedItem.localCopyVersionIdentifier = uploadItem.itemVersionIdentifier; - uploadedItem.parentFileID = uploadItem.parentFileID; - - uploadedItem.previousPlaceholderFileID = uploadedItem.fileID; - - // Update uploaded item with local relative path - syncContext.addedItems = @[ uploadedItem ]; - - // Remove placeholder item - syncContext.removedItems = @[ uploadItem ]; - - // Remove sync record from placeholder - [uploadItem removeSyncRecordID:syncContext.syncRecord.recordID activity:OCItemSyncActivityUploading]; - - // Remove placeholder directory (may still contain a copy of the file after all) if no other sync records are active on it - if (uploadItem.activeSyncRecordIDs.count == 0) - { - [[NSFileManager defaultManager] removeItemAtURL:placeholderItemContainerURL error:&error]; - } - } - else - { - // => Error moving placeholder item file to uploaded item file - OCLogWarning(@"Upload completion failed moving file of placeholder (%@) to final destination (%@): %@", OCLogPrivate(uploadItemURL), OCLogPrivate(uploadedItemURL), OCLogPrivate(error)); - } - } - else - { - OCLogWarning(@"Upload completion failed retrieving placeholder and upload URLs"); - } + uploadedItem.previousPlaceholderFileID = uploadItem.fileID; } - else - { - // Upload from modified item complete! - // Prepare uploadedItem to replace uploadItem - [uploadedItem prepareToReplace:uploadItem]; + // Prepare uploadedItem to replace uploadItem + [uploadedItem prepareToReplace:uploadItem]; - // Update uploaded item with local relative path - uploadedItem.localRelativePath = [self.core.vault relativePathForItem:uploadedItem]; + // Update uploaded item with local relative path + uploadedItem.localRelativePath = [self.core.vault relativePathForItem:uploadedItem]; - // Compute checksum to determine if the current main file of this file is identical to this upload action's version - OCSyncExec(checksumComputation, { - [OCChecksum computeForFile:[self.core localURLForItem:uploadedItem] checksumAlgorithm:self.importFileChecksum.algorithmIdentifier completionHandler:^(NSError *error, OCChecksum *computedChecksum) { - // Set locallyModified to NO if checksums match, YES if they don't - uploadedItem.locallyModified = ![self.importFileChecksum isEqual:computedChecksum]; + // Compute checksum to determine if the current main file of this file is identical to this upload action's version + OCSyncExec(checksumComputation, { + [OCChecksum computeForFile:[self.core localURLForItem:uploadedItem] checksumAlgorithm:self.importFileChecksum.algorithmIdentifier completionHandler:^(NSError *error, OCChecksum *computedChecksum) { + // Set locallyModified to NO if checksums match, YES if they don't + uploadedItem.locallyModified = ![self.importFileChecksum isEqual:computedChecksum]; - OCSyncExecDone(checksumComputation); - }]; - }); + OCSyncExecDone(checksumComputation); + }]; + }); - // Remove sync record from placeholder - [uploadedItem removeSyncRecordID:syncContext.syncRecord.recordID activity:OCItemSyncActivityUploading]; + // Add version information if local and uploaded item version are identical + if (!uploadedItem.locallyModified) + { + uploadedItem.localCopyVersionIdentifier = uploadedItem.itemVersionIdentifier; + } - // Indicate item update - syncContext.updatedItems = @[ uploadedItem ]; + // Remove sync record from placeholder + [uploadedItem removeSyncRecordID:syncContext.syncRecord.recordID activity:OCItemSyncActivityUploading]; - // Update localItem - self.localItem = uploadedItem; + // Indicate item update + syncContext.updatedItems = @[ uploadedItem ]; - // Remove temporary copy - if (_importFileIsTemporaryAlongsideCopy) - { - NSError *error; + // Update localItem + self.localItem = uploadedItem; - [[NSFileManager defaultManager] removeItemAtURL:_importFileURL error:&error]; - } + // Remove temporary copy + if (_importFileIsTemporaryAlongsideCopy) + { + NSError *error; + + [[NSFileManager defaultManager] removeItemAtURL:_importFileURL error:&error]; } } else { - OCLogWarning(@"Upload completion failed retrieving placeholder item"); + OCLogWarning(@"Upload completion failed retrieving localItem/placeholder"); } // Action complete and can be removed diff --git a/ownCloudSDK/Core/Sync/OCCore+SyncEngine.h b/ownCloudSDK/Core/Sync/OCCore+SyncEngine.h index c4b416c7..05382c57 100644 --- a/ownCloudSDK/Core/Sync/OCCore+SyncEngine.h +++ b/ownCloudSDK/Core/Sync/OCCore+SyncEngine.h @@ -72,6 +72,12 @@ typedef void(^OCCoreSyncIssueResolutionResultHandler)(OCSyncIssueChoice *choice) - (NSError *)_descheduleSyncRecord:(OCSyncRecord *)syncRecord completeWithError:(NSError *)completionError parameter:(id)parameter; - (NSError *)_rescheduleSyncRecord:(OCSyncRecord *)syncRecord withUpdates:(NSError *(^)(OCSyncRecord *record))applyUpdates; +#pragma mark - Sync record persistence +- (void)addSyncRecords:(NSArray *)syncRecords completionHandler:(OCDatabaseCompletionHandler)completionHandler; +- (void)updateSyncRecords:(NSArray *)syncRecords completionHandler:(OCDatabaseCompletionHandler)completionHandler; +- (void)removeSyncRecords:(NSArray *)syncRecords completionHandler:(OCDatabaseCompletionHandler)completionHandler; +- (void)publishInitialSyncRecordActivities; + @end extern OCEventUserInfoKey OCEventUserInfoKeySyncRecordID; diff --git a/ownCloudSDK/Core/Sync/OCCore+SyncEngine.m b/ownCloudSDK/Core/Sync/OCCore+SyncEngine.m index 17478200..19100a0e 100644 --- a/ownCloudSDK/Core/Sync/OCCore+SyncEngine.m +++ b/ownCloudSDK/Core/Sync/OCCore+SyncEngine.m @@ -33,6 +33,7 @@ #import "OCIssue+SyncIssue.h" #import "OCWaitCondition.h" #import "OCProcessManager.h" +#import "OCSyncRecordActivity.h" OCIPCNotificationName OCIPCNotificationNameProcessSyncRecordsBase = @"org.owncloud.process-sync-records"; @@ -51,6 +52,8 @@ - (void)setupSyncEngine [OCIPNotificationCenter.sharedNotificationCenter addObserver:self forName:notificationName withHandler:^(OCIPNotificationCenter * _Nonnull notificationCenter, OCCore * _Nonnull core, OCIPCNotificationName _Nonnull notificationName) { [core setNeedsToProcessSyncRecords]; }]; + + [self publishInitialSyncRecordActivities]; } - (void)shutdownSyncEngine @@ -184,6 +187,7 @@ - (NSProgress *)_enqueueSyncRecordWithAction:(OCSyncAction *)action resultHandle if (action != nil) { progress = [NSProgress indeterminateProgress]; + progress.cancellable = NO; syncRecord = [[OCSyncRecord alloc] initWithAction:action resultHandler:resultHandler]; @@ -203,7 +207,7 @@ - (void)submitSyncRecord:(OCSyncRecord *)record __block NSError *blockError = nil; // Add sync record to database (=> ensures it is persisted and has a recordID) - [self.vault.database addSyncRecords:@[ record ] completionHandler:^(OCDatabase *db, NSError *error) { + [self addSyncRecords:@[ record ] completionHandler:^(OCDatabase *db, NSError *error) { blockError = error; }]; @@ -258,7 +262,7 @@ - (void)submitSyncRecord:(OCSyncRecord *)record if (record.recordID != nil) { // Record still has a recordID, so wasn't included in syncContext.removeRecords. Remove now. - [self.vault.database removeSyncRecords:@[ record ] completionHandler:nil]; + [self removeSyncRecords:@[ record ] completionHandler:nil]; } // Call result handler @@ -284,7 +288,7 @@ - (NSError *)_rescheduleSyncRecord:(OCSyncRecord *)syncRecord withUpdates:(NSErr { [syncRecord transitionToState:OCSyncRecordStateReady withWaitConditions:nil]; - [self.vault.database updateSyncRecords:@[syncRecord] completionHandler:^(OCDatabase *db, NSError *updateError) { + [self updateSyncRecords:@[syncRecord] completionHandler:^(OCDatabase *db, NSError *updateError) { error = updateError; }]; @@ -317,7 +321,7 @@ - (NSError *)_descheduleSyncRecord:(OCSyncRecord *)syncRecord completeWithError: OCLogDebug(@"descheduling record %@ (parameter=%@, error=%@)", syncRecord, parameter, completionError); - [self.vault.database removeSyncRecords:@[syncRecord] completionHandler:^(OCDatabase *db, NSError *removeError) { + [self removeSyncRecords:@[syncRecord] completionHandler:^(OCDatabase *db, NSError *removeError) { error = removeError; }]; @@ -416,6 +420,10 @@ - (void)processSyncRecords { [self beginActivity:@"process sync records"]; + OCWaitInitAndStartTask(processSyncRecords); + + [self dumpSyncJournalWithTags:@[@"BeforeProc"]]; + [self performProtectedSyncBlock:^NSError *{ __block NSError *error = nil; __block BOOL stopProcessing = NO; @@ -446,6 +454,10 @@ - (void)processSyncRecords // Process sync record nextInstruction = [self processSyncRecord:syncRecord error:&error]; + OCLogDebug(@"Processing of sync record finished with nextInstruction=%d", nextInstruction); + + [self dumpSyncJournalWithTags:@[@"PostProc"]]; + // Perform sync record result instruction switch (nextInstruction) { @@ -471,7 +483,7 @@ - (void)processSyncRecords case OCCoreSyncInstructionDeleteLast: // Delete record - [db removeSyncRecords:@[ syncRecord ] completionHandler:^(OCDatabase *db, NSError *dbError) { + [self removeSyncRecords:@[ syncRecord ] completionHandler:^(OCDatabase *db, NSError *dbError) { if (dbError != nil) { error = dbError; @@ -505,8 +517,14 @@ - (void)processSyncRecords // OCLogWarning(@"Outstanding events after completing sync record processing while sync records need to be processed"); // } + OCWaitDidFinishTask(processSyncRecords); + [self endActivity:@"process sync records"]; }]; + + OCWaitForCompletion(processSyncRecords); + + [self dumpSyncJournalWithTags:@[@"AfterProc"]]; } - (BOOL)processWaitRecordsOfSyncRecord:(OCSyncRecord *)syncRecord error:(NSError **)outError @@ -607,7 +625,7 @@ - (BOOL)processWaitRecordsOfSyncRecord:(OCSyncRecord *)syncRecord error:(NSError if (updateSyncRecordInDB) { - [self.database updateSyncRecords:@[ syncRecord ] completionHandler:^(OCDatabase *db, NSError *dbError) { + [self updateSyncRecords:@[ syncRecord ] completionHandler:^(OCDatabase *db, NSError *dbError) { error = dbError; }]; } @@ -649,37 +667,6 @@ - (OCCoreSyncInstruction)processSyncRecord:(OCSyncRecord *)syncRecord error:(NSE } } - // Process sync record cancellation - if (syncRecord.progress.cancelled) - { - OCSyncAction *syncAction; - - OCLogDebug(@"record %@ has been cancelled - notifying", OCLogPrivate(syncRecord)); - - if ((syncAction = syncRecord.action) != nil) - { - OCSyncContext *syncContext = [OCSyncContext descheduleContextWithSyncRecord:syncRecord]; - - OCLogDebug(@"record %@ will be cancelled", OCLogPrivate(syncRecord)); - - syncContext.error = OCError(OCErrorCancelled); // consumed by -cancelWithContext: - - error = [self processWithContext:syncContext block:^NSError *(OCSyncAction *action) { - doNext = [action cancelWithContext:syncContext]; - return(nil); - }]; - - OCLogDebug(@"record %@ cancelled with error %@", OCLogPrivate(syncRecord), OCLogPrivate(syncContext.error)); - } - else - { - // Deschedule & call resultHandler - [self _descheduleSyncRecord:syncRecord completeWithError:OCError(OCErrorCancelled) parameter:nil]; - } - - return (doNext); - } - // Skip sync records without an ID (should never happen, actually) if (syncRecord.recordID == nil) { @@ -733,6 +720,37 @@ - (OCCoreSyncInstruction)processSyncRecord:(OCSyncRecord *)syncRecord error:(NSE } } + // Process sync record cancellation + if (syncRecord.progress.cancelled) + { + OCSyncAction *syncAction; + + OCLogDebug(@"record %@ has been cancelled - notifying", OCLogPrivate(syncRecord)); + + if ((syncAction = syncRecord.action) != nil) + { + OCSyncContext *syncContext = [OCSyncContext descheduleContextWithSyncRecord:syncRecord]; + + OCLogDebug(@"record %@ will be cancelled", OCLogPrivate(syncRecord)); + + syncContext.error = OCError(OCErrorCancelled); // consumed by -cancelWithContext: + + error = [self processWithContext:syncContext block:^NSError *(OCSyncAction *action) { + doNext = [action cancelWithContext:syncContext]; + return(nil); + }]; + + OCLogDebug(@"record %@ cancelled with error %@", OCLogPrivate(syncRecord), OCLogPrivate(syncContext.error)); + } + else + { + // Deschedule & call resultHandler + [self _descheduleSyncRecord:syncRecord completeWithError:OCError(OCErrorCancelled) parameter:nil]; + } + + return (doNext); + } + // Process sync record's wait conditions if (![self processWaitRecordsOfSyncRecord:syncRecord error:outError]) { @@ -866,12 +884,12 @@ - (void)performSyncContextActions:(OCSyncContext *)syncContext beforeQueryUpdateAction = ^(dispatch_block_t completionHandler){ if (syncContext.removeRecords != nil) { - [self.vault.database removeSyncRecords:syncContext.removeRecords completionHandler:nil]; + [self removeSyncRecords:syncContext.removeRecords completionHandler:nil]; } if (syncContext.updateStoredSyncRecordAfterItemUpdates) { - [self.vault.database updateSyncRecords:@[ syncContext.syncRecord ] completionHandler:nil]; + [self updateSyncRecords:@[ syncContext.syncRecord ] completionHandler:nil]; } completionHandler(); @@ -985,24 +1003,69 @@ - (OCEventTarget *)_eventTargetWithSyncRecord:(OCSyncRecord *)syncRecord return ([self _eventTargetWithSyncRecord:syncRecord userInfo:nil ephermal:nil]); } +#pragma mark - Sync record persistence +- (void)addSyncRecords:(NSArray *)syncRecords completionHandler:(OCDatabaseCompletionHandler)completionHandler +{ + [self.database addSyncRecords:syncRecords completionHandler:completionHandler]; + + for (OCSyncRecord *syncRecord in syncRecords) + { + [self.activityManager update:[OCActivityUpdate publishingActivityFor:syncRecord]]; + } +} + +- (void)updateSyncRecords:(NSArray *)syncRecords completionHandler:(OCDatabaseCompletionHandler)completionHandler; +{ + for (OCSyncRecord *syncRecord in syncRecords) + { + [self.activityManager update:[[[OCActivityUpdate updatingActivityFor:syncRecord] withRecordState:syncRecord.state] withProgress:syncRecord.progress]]; + } + + [self.database updateSyncRecords:syncRecords completionHandler:completionHandler]; +} + +- (void)removeSyncRecords:(NSArray *)syncRecords completionHandler:(OCDatabaseCompletionHandler)completionHandler; +{ + for (OCSyncRecord *syncRecord in syncRecords) + { + [self.activityManager update:[OCActivityUpdate unpublishActivityFor:syncRecord]]; + } + + [self.database removeSyncRecords:syncRecords completionHandler:completionHandler]; +} + +- (void)publishInitialSyncRecordActivities +{ + [self.database retrieveSyncRecordsForPath:nil action:nil inProgressSince:nil completionHandler:^(OCDatabase *db, NSError *error, NSArray *syncRecords) { + for (OCSyncRecord *syncRecord in syncRecords) + { + syncRecord.action.core = self; + [self.activityManager update:[OCActivityUpdate publishingActivityFor:syncRecord]]; + } + }]; +} + #pragma mark - Sync debugging -- (void)dumpSyncJournal +- (void)dumpSyncJournalWithTags:(NSArray *)tags { - OCSyncExec(journalDump, { - [self.database retrieveSyncRecordsForPath:nil action:nil inProgressSince:nil completionHandler:^(OCDatabase *db, NSError *error, NSArray *syncRecords) { - OCLogDebug(@"Sync Journal Dump:"); - OCLogDebug(@"=================="); + if (OCLogger.logLevel <= OCLogLevelDebug) + { + OCSyncExec(journalDump, { + [self.database retrieveSyncRecordsForPath:nil action:nil inProgressSince:nil completionHandler:^(OCDatabase *db, NSError *error, NSArray *syncRecords) { + OCTLogDebug(tags, @"Sync Journal Dump:"); + OCTLogDebug(tags, @"=================="); - for (OCSyncRecord *record in syncRecords) - { - OCLogDebug(@"%@ | %@ | %@", [[record.recordID stringValue] rightPaddedMinLength:5], - [record.actionIdentifier leftPaddedMinLength:20], - [[record.inProgressSince description] leftPaddedMinLength:20]); - } + for (OCSyncRecord *record in syncRecords) + { + OCTLogDebug(tags, @"%@ | %@ | %@", [[record.recordID stringValue] rightPaddedMinLength:5], + [record.actionIdentifier leftPaddedMinLength:20], + [[record.inProgressSince description] leftPaddedMinLength:20]); + } - OCSyncExecDone(journalDump); - }]; - }); + OCSyncExecDone(journalDump); + }]; + }); + } } @end diff --git a/ownCloudSDK/Core/Sync/Record/OCSyncRecord.h b/ownCloudSDK/Core/Sync/Record/OCSyncRecord.h index 154c8a59..f5605af9 100644 --- a/ownCloudSDK/Core/Sync/Record/OCSyncRecord.h +++ b/ownCloudSDK/Core/Sync/Record/OCSyncRecord.h @@ -22,6 +22,7 @@ #import "OCTypes.h" #import "OCCore.h" #import "OCLogger.h" +#import "OCActivity.h" NS_ASSUME_NONNULL_BEGIN @@ -38,11 +39,13 @@ typedef NS_ENUM(NSInteger, OCSyncRecordState) OCSyncRecordStateFailed //!< Sync record's action failed unrecoverably }; -@interface OCSyncRecord : NSObject +@interface OCSyncRecord : NSObject { OCSyncRecordID _recordID; OCProcessSession *_originProcessSession; + OCActivityIdentifier _activityIdentifier; + OCSyncActionIdentifier _actionIdentifier; OCSyncAction *_action; NSDate *_timestamp; @@ -65,6 +68,7 @@ typedef NS_ENUM(NSInteger, OCSyncRecordState) @property(readonly) OCSyncActionIdentifier actionIdentifier; //!< The action @property(strong) OCSyncAction *action; //!< The sync action @property(readonly) NSDate *timestamp; //!< Time the action was triggered +@property(readonly,nonatomic,nullable) OCLocalID localID; //!< The localID of the item targeted by the action #pragma mark - Scheduling and processing tracking @property(readonly,nonatomic) OCSyncRecordState state; //!< Current processing state diff --git a/ownCloudSDK/Core/Sync/Record/OCSyncRecord.m b/ownCloudSDK/Core/Sync/Record/OCSyncRecord.m index 96a2fcfc..25c66403 100644 --- a/ownCloudSDK/Core/Sync/Record/OCSyncRecord.m +++ b/ownCloudSDK/Core/Sync/Record/OCSyncRecord.m @@ -22,6 +22,7 @@ #import "OCSyncIssue.h" #import "OCProcessManager.h" #import "OCWaitConditionIssue.h" +#import "OCSyncRecordActivity.h" @implementation OCSyncRecord @@ -68,6 +69,11 @@ - (void)setState:(OCSyncRecordState)state _state = state; } +- (OCLocalID)localID +{ + return (self.action.localItem.localID); +} + #pragma mark - Serialization + (instancetype)syncRecordFromSerializedData:(NSData *)serializedData { @@ -230,6 +236,22 @@ - (void)encodeWithCoder:(NSCoder *)coder [coder encodeObject:_waitConditions forKey:@"waitConditions"]; } +#pragma mark - Activity Source +- (OCActivityIdentifier)activityIdentifier +{ + if (_activityIdentifier == nil) + { + _activityIdentifier = [NSString stringWithFormat:@"syncRecord:%@", _recordID]; + } + + return (_activityIdentifier); +} + +- (OCActivity *)provideActivity +{ + return ([[OCSyncRecordActivity alloc] initWithSyncRecord:self identifier:self.activityIdentifier]); +} + #pragma mark - Progress setup - (void)setProgress:(NSProgress *)progress { diff --git a/ownCloudSDK/Core/Sync/Record/OCSyncRecordStatus.h b/ownCloudSDK/Core/Sync/Record/OCSyncRecordStatus.h deleted file mode 100644 index 060a39c3..00000000 --- a/ownCloudSDK/Core/Sync/Record/OCSyncRecordStatus.h +++ /dev/null @@ -1,29 +0,0 @@ -// -// OCSyncRecordStatus.h -// ownCloudSDK -// -// Created by Felix Schwarz on 19.12.18. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -#import -#import "OCCore.h" -#import "OCSyncRecord.h" -#import "OCTypes.h" -#import "OCLogTag.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface OCSyncRecordStatus : NSObject - -@property(strong) OCSyncRecordID recordID; - -@property(assign) OCEventType type; -@property(assign) OCSyncRecordState state; - -@property(strong,nullable) NSString *localizedDescription; -@property(strong,nullable) NSProgress *progress; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ownCloudSDK/Core/Sync/Record/OCSyncRecordStatus.m b/ownCloudSDK/Core/Sync/Record/OCSyncRecordStatus.m deleted file mode 100644 index 9fe79c8e..00000000 --- a/ownCloudSDK/Core/Sync/Record/OCSyncRecordStatus.m +++ /dev/null @@ -1,13 +0,0 @@ -// -// OCSyncRecordStatus.m -// ownCloudSDK -// -// Created by Felix Schwarz on 19.12.18. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -#import "OCSyncRecordStatus.h" - -@implementation OCSyncRecordStatus - -@end diff --git a/ownCloudSDK/Errors/NSError+OCError.h b/ownCloudSDK/Errors/NSError+OCError.h index bfa0245a..8aa2997b 100644 --- a/ownCloudSDK/Errors/NSError+OCError.h +++ b/ownCloudSDK/Errors/NSError+OCError.h @@ -63,6 +63,8 @@ typedef NS_ENUM(NSUInteger, OCError) OCErrorItemAlreadyExists, //!< There already is an item at the destination of this action OCErrorItemNotAvailableOffline, //!< This item is not available offline + OCErrorFileNotFound, //!< The file was not found. + OCErrorSyncRecordNotFound, //!< The referenced sync record could not be found. OCErrorNewerVersionExists, //!< A newer version already exists diff --git a/ownCloudSDK/Errors/NSError+OCError.m b/ownCloudSDK/Errors/NSError+OCError.m index af87183c..2a7e77b0 100644 --- a/ownCloudSDK/Errors/NSError+OCError.m +++ b/ownCloudSDK/Errors/NSError+OCError.m @@ -185,6 +185,10 @@ + (id)provideUserInfoValueForOCError:(NSError *)error userInfoKey:(NSErrorUserIn unlocalizedString = @"Item not available offline."; break; + case OCErrorFileNotFound: + unlocalizedString = @"File not found."; + break; + case OCErrorCancelled: unlocalizedString = @"The operation was cancelled."; break; diff --git a/ownCloudSDK/Core/Activity/NSProgress+OCEvent.h b/ownCloudSDK/Events/NSProgress+OCEvent.h similarity index 92% rename from ownCloudSDK/Core/Activity/NSProgress+OCEvent.h rename to ownCloudSDK/Events/NSProgress+OCEvent.h index 87bc1dba..f8df5a32 100644 --- a/ownCloudSDK/Core/Activity/NSProgress+OCEvent.h +++ b/ownCloudSDK/Events/NSProgress+OCEvent.h @@ -27,6 +27,9 @@ - (OCFileID)fileID; - (void)setFileID:(OCFileID)fileID; +- (OCLocalID)localID; +- (void)setLocalID:(OCLocalID)localID; + - (OCConnectionJobID)jobID; - (void)setJobID:(OCConnectionJobID)jobID; diff --git a/ownCloudSDK/Core/Activity/NSProgress+OCEvent.m b/ownCloudSDK/Events/NSProgress+OCEvent.m similarity index 85% rename from ownCloudSDK/Core/Activity/NSProgress+OCEvent.m rename to ownCloudSDK/Events/NSProgress+OCEvent.m index 539ff9c4..2854cb20 100644 --- a/ownCloudSDK/Core/Activity/NSProgress+OCEvent.m +++ b/ownCloudSDK/Events/NSProgress+OCEvent.m @@ -40,7 +40,17 @@ - (void)setFileID:(OCFileID)fileID [self setUserInfoObject:fileID forKey:@"_fileID"]; } -- (OCConnectionJobID)jobID; +- (OCLocalID)localID +{ + return (self.userInfo[@"_localID"]); +} + +- (void)setLocalID:(OCLocalID)localID +{ + [self setUserInfoObject:localID forKey:@"_localID"]; +} + +- (OCConnectionJobID)jobID { return (self.userInfo[@"_jobID"]); } diff --git a/ownCloudSDK/Item/OCItem.h b/ownCloudSDK/Item/OCItem.h index 13d18bd8..3a492811 100644 --- a/ownCloudSDK/Item/OCItem.h +++ b/ownCloudSDK/Item/OCItem.h @@ -69,6 +69,14 @@ typedef NS_ENUM(NSInteger, OCItemThumbnailAvailability) OCItemThumbnailAvailabilityInternal = -1 //!< Internal value. Don't use. }; +typedef NS_ENUM(NSInteger, OCItemCloudStatus) +{ + OCItemCloudStatusCloudOnly, //!< Item is only stored remotely (no local copy) + OCItemCloudStatusLocalCopy, //!< Item is a local copy of a file on the server + OCItemCloudStatusLocallyModified, //!< Item is a modified copy of a file on the server + OCItemCloudStatusLocalOnly //!< Item only exists locally. There's no remote copy. +}; + NS_ASSUME_NONNULL_BEGIN @interface OCItem : NSObject @@ -89,6 +97,8 @@ NS_ASSUME_NONNULL_BEGIN @property(assign) OCItemStatus status; //!< the status of the item (exists/at rest, is transient) +@property(readonly,nonatomic) OCItemCloudStatus cloudStatus; //!< the cloud status of the item (computed using the item's metadata) + @property(assign) BOOL removed; //!< whether the item has been removed (defaults to NO) (stored by database, ephermal otherwise) @property(nullable,strong) NSProgress *progress; //!< If status is transient, a progress describing the status (ephermal) @@ -106,6 +116,9 @@ NS_ASSUME_NONNULL_BEGIN @property(nullable,strong) OCPath previousPath; //!< A previous path of the item, f.ex. before being moved (dynamic/ephermal) @property(nullable,strong) OCFileID previousPlaceholderFileID; //!< FileID of placeholder that was replaced by this item for giving hints to the Sync Engine, so it can inform subsequent sync actions depending on the replaced placeholder (dynamic/ephermal) +@property(nullable,strong) OCLocalID parentLocalID; //!< Unique local identifier of the parent folder (persists over lifetime of item, incl. across modifications and placeholder -> item transitions) +@property(nullable,strong) OCLocalID localID; //!< Unique local identifier of the item (persists over lifetime of item, incl. across modifications and placeholder -> item transitions) + @property(nullable,strong,nonatomic) OCFileID parentFileID; //!< Unique identifier of the parent folder (persists over lifetime of file, incl. across modifications) @property(nullable,strong,nonatomic) OCFileID fileID; //!< Unique identifier of the item on the server (persists over lifetime of file, incl. across modifications) @property(nullable,strong,nonatomic) OCFileETag eTag; //!< ETag of the item on the server (changes with every modification) @@ -131,6 +144,8 @@ NS_ASSUME_NONNULL_BEGIN @property(nullable,strong) OCDatabaseID databaseID; //!< OCDatabase-specific ID referencing the item in the database ++ (OCLocalID)generateNewLocalID; //!< Generates a new, unique OCLocalID + + (instancetype)placeholderItemOfType:(OCItemType)type; + (nullable NSString *)localizedNameForProperty:(OCItemPropertyName)propertyName; diff --git a/ownCloudSDK/Item/OCItem.m b/ownCloudSDK/Item/OCItem.m index 024251b7..8d9d155f 100644 --- a/ownCloudSDK/Item/OCItem.m +++ b/ownCloudSDK/Item/OCItem.m @@ -25,6 +25,13 @@ @implementation OCItem +@dynamic cloudStatus; + ++ (OCLocalID)generateNewLocalID +{ + return [[NSUUID new].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""]; +} + #pragma mark - Placeholder + (instancetype)placeholderItemOfType:(OCItemType)type { @@ -84,6 +91,9 @@ - (void)encodeWithCoder:(NSCoder *)coder [coder encodeObject:_path forKey:@"path"]; + [coder encodeObject:_parentLocalID forKey:@"parentLocalID"]; + [coder encodeObject:_localID forKey:@"localID"]; + [coder encodeObject:_parentFileID forKey:@"parentFileID"]; [coder encodeObject:_fileID forKey:@"fileID"]; [coder encodeObject:_eTag forKey:@"eTag"]; @@ -113,6 +123,7 @@ - (instancetype)init [self _captureCallstack]; _thumbnailAvailability = OCItemThumbnailAvailabilityInternal; + _localID = [OCItem generateNewLocalID]; } return (self); @@ -142,6 +153,9 @@ - (instancetype)initWithCoder:(NSCoder *)decoder _path = [decoder decodeObjectOfClass:[NSString class] forKey:@"path"]; + _parentLocalID = [decoder decodeObjectOfClass:[NSString class] forKey:@"parentLocalID"]; + _localID = [decoder decodeObjectOfClass:[NSString class] forKey:@"localID"]; + _parentFileID = [decoder decodeObjectOfClass:[NSString class] forKey:@"parentFileID"]; _fileID = [decoder decodeObjectOfClass:[NSString class] forKey:@"fileID"]; _eTag = [decoder decodeObjectOfClass:[NSString class] forKey:@"eTag"]; @@ -283,6 +297,33 @@ - (void)setValue:(id)value forLocalAttribute:(OCLocalAttribute)localAttribute } } +#pragma mark - Cloud status +- (OCItemCloudStatus)cloudStatus +{ + if (self.localRelativePath != nil) + { + if (self.locallyModified) + { + if (self.isPlaceholder) + { + return (OCItemCloudStatusLocalOnly); + } + else + { + return (OCItemCloudStatusLocallyModified); + } + } + else + { + return (OCItemCloudStatusLocalCopy); + } + } + else + { + return (OCItemCloudStatusCloudOnly); + } +} + #pragma mark - Sync record tools - (void)addSyncRecordID:(OCSyncRecordID)syncRecordID activity:(OCItemSyncActivity)activity { @@ -356,6 +397,10 @@ - (void)prepareToReplace:(OCItem *)item self.parentFileID = item.parentFileID; } + self.parentLocalID = item.parentLocalID; + self.localID = item.localID; + // Also set item.localID to nil here?! + // Make sure to use latest version of local attributes if (self.localAttributesLastModified < item.localAttributesLastModified) { @@ -443,7 +488,7 @@ - (OCFile *)fileWithCore:(OCCore *)core #pragma mark - Description - (NSString *)description { - return ([NSString stringWithFormat:@"<%@: %p, type: %lu, name: %@, path: %@, size: %lu bytes, MIME-Type: %@, Last modified: %@, fileID: %@, eTag: %@, parentID: %@%@>", NSStringFromClass(self.class), self, (unsigned long)self.type, self.name, self.path, self.size, self.mimeType, self.lastModified, self.fileID, self.eTag, self.parentFileID, (_removed ? @", removed" : @"")]); + return ([NSString stringWithFormat:@"<%@: %p, type: %lu, name: %@, path: %@, size: %lu bytes, MIME-Type: %@, Last modified: %@, fileID: %@, eTag: %@, parentID: %@, localID: %@, parentLocalID: %@%@>", NSStringFromClass(self.class), self, (unsigned long)self.type, self.name, self.path, self.size, self.mimeType, self.lastModified, self.fileID, self.eTag, self.parentFileID, self.localID, self.parentLocalID, (_removed ? @", removed" : @"")]); } #pragma mark - Copying diff --git a/ownCloudSDK/OCMacros.h b/ownCloudSDK/OCMacros.h index 62739073..07aaace1 100644 --- a/ownCloudSDK/OCMacros.h +++ b/ownCloudSDK/OCMacros.h @@ -32,10 +32,11 @@ #define OCWaitForCompletionWithTimeout(label,timeout) dispatch_group_wait(label, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC))) // Macros to simplify the use of async APIs in a synchronous fashion -#define OCSyncExec(label,code) OCWaitInitAndStartTask(label); \ +#define OCSyncExec(label,code) dispatch_semaphore_t label = dispatch_semaphore_create(0); \ code \ - OCWaitForCompletion(label) -#define OCSyncExecDone(label) OCWaitDidFinishTask(label) + dispatch_semaphore_wait(label, DISPATCH_TIME_FOREVER) + +#define OCSyncExecDone(label) dispatch_semaphore_signal(label) #define OCTypedCast(var,className) ([var isKindOfClass:[className class]] ? ((className *)var) : nil) diff --git a/ownCloudSDK/OCTypes.h b/ownCloudSDK/OCTypes.h index 6cbc3184..e7790bdf 100644 --- a/ownCloudSDK/OCTypes.h +++ b/ownCloudSDK/OCTypes.h @@ -21,7 +21,9 @@ typedef NSString* OCPath; //!< NSString representing the path relative to the server's root directory. -typedef NSString* OCFileID; //!< Unique identifier of the item on the server (persists over lifetime of file, incl. across modifications) (files only) +typedef NSString* OCLocalID; //!< Unique local identifier of the item (persists over lifetime of file, incl. across modifications and placeholder -> item transition). + +typedef NSString* OCFileID; //!< Unique identifier of the item on the server (persists over lifetime of file, incl. across modifications) (files and folders) typedef NSString* OCFileETag; //!< Identifier unique to a specific combination of contents and metadata. Can be used to detect changes. (files and folders) typedef NSString* OCLocalAttribute NS_TYPED_ENUM; //!< Identifier uniquely identifying a local attribute diff --git a/ownCloudSDK/Resource Management/OCBookmarkManager.h b/ownCloudSDK/Resource Management/OCBookmarkManager.h index 7bf48cda..877f3dde 100644 --- a/ownCloudSDK/Resource Management/OCBookmarkManager.h +++ b/ownCloudSDK/Resource Management/OCBookmarkManager.h @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN NSMutableArray *_bookmarks; } -@property(strong) NSMutableArray *bookmarks; +@property(strong) NSArray *bookmarks; @property(readonly,nonatomic) NSURL *bookmarkStoreURL; #pragma mark - Shared instance diff --git a/ownCloudSDK/Resource Management/OCBookmarkManager.m b/ownCloudSDK/Resource Management/OCBookmarkManager.m index f3c5927a..9f9bab3f 100644 --- a/ownCloudSDK/Resource Management/OCBookmarkManager.m +++ b/ownCloudSDK/Resource Management/OCBookmarkManager.m @@ -44,11 +44,18 @@ - (instancetype)init if ((self = [super init]) != nil) { _bookmarks = [NSMutableArray new]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleBookmarkUpdatedNotification:) name:OCBookmarkUpdatedNotification object:nil]; } return(self); } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:OCBookmarkUpdatedNotification object:nil]; +} + #pragma mark - Bookmark storage - (NSURL *)bookmarkStoreURL { @@ -123,6 +130,19 @@ - (void)postChangeNotification [NSNotificationCenter.defaultCenter postNotificationName:OCBookmarkManagerListChanged object:nil]; } +- (void)handleBookmarkUpdatedNotification:(NSNotification *)updateNotification +{ + if (updateNotification.object == nil) { return; } + + @synchronized(self) + { + if ([_bookmarks indexOfObjectIdenticalTo:updateNotification.object] != NSNotFound) + { + [self saveBookmarks]; + } + } +} + #pragma mark - List mutations - (void)addBookmark:(OCBookmark *)bookmark { diff --git a/ownCloudSDK/Resources/en.lproj/Localizable.strings b/ownCloudSDK/Resources/en.lproj/Localizable.strings index 7bbc733b..4691d98a 100644 --- a/ownCloudSDK/Resources/en.lproj/Localizable.strings +++ b/ownCloudSDK/Resources/en.lproj/Localizable.strings @@ -202,6 +202,9 @@ // OCErrorItemNotAvailableOffline "Item not available offline." = "Item not available offline."; +// OCErrorFileNotFound: +"File not found." = "File not found."; + // OCErrorCancelled "The operation was cancelled." = "The operation was cancelled."; diff --git a/ownCloudSDK/Vaults/Database/OCDatabase+Schemas.m b/ownCloudSDK/Vaults/Database/OCDatabase+Schemas.m index 9ead8095..7b81ff92 100644 --- a/ownCloudSDK/Vaults/Database/OCDatabase+Schemas.m +++ b/ownCloudSDK/Vaults/Database/OCDatabase+Schemas.m @@ -260,6 +260,164 @@ - (void)addOrUpdateMetaDataSchema }]]; }] ]; + + // Version 5 + [self.sqlDB addTableSchema:[OCSQLiteTableSchema + schemaWithTableName:OCDatabaseTableNameMetaData + version:5 + creationQueries:@[ + /* + mdID : INTEGER - unique ID used to uniquely identify and efficiently update a row + type : INTEGER - OCItemType value to indicate if this is a file or a collection/folder + syncAnchor: INTEGER - sync anchor, a number that increases its value with every change to an entry. For files, higher sync anchor values indicate the file changed (incl. creation, content or meta data changes). For collections/folders, higher sync anchor values indicate the list of items in the collection/folder changed in a way not covered by file entries (i.e. rename, deletion, but not creation of files). + removed : INTEGER - value indicating if this file or folder has been removed: 1 if it was, 0 if not (default). Removed entries are kept around until their delta to the latest syncAnchor value exceeds -[OCDatabase removedItemRetentionLength]. + locallyModified: INTEGER- value indicating if this is a file that's been created or modified locally + localRelativePath: TEXT - path of the local copy of the item, relative to the rootURL of the vault that stores it + path : TEXT - full path of the item (e.g. "/example/file.txt") + parentPath : TEXT - parent path of the item. (e.g. "/example" for an item at "/example/file.txt") + name : TEXT - name of the item (e.g. "file.txt" for an item at "/example/file.txt") + fileID : TEXT - OCFileID identifying the item + localID : TEXT - OCLocalID identifying the item + itemData : BLOB - data of the serialized OCItem + */ + @"CREATE TABLE metaData (mdID INTEGER PRIMARY KEY, type INTEGER NOT NULL, syncAnchor INTEGER NOT NULL, removed INTEGER NOT NULL, locallyModified INTEGER NOT NULL, localRelativePath TEXT NULL, path TEXT NOT NULL, parentPath TEXT NOT NULL, name TEXT NOT NULL, fileID TEXT NOT NULL, localID TEXT, itemData BLOB NOT NULL)", + + // Create indexes over path and parentPath + @"CREATE INDEX idx_metaData_path ON metaData (path)", + @"CREATE INDEX idx_metaData_parentPath ON metaData (parentPath)", + @"CREATE INDEX idx_metaData_synchAnchor ON metaData (syncAnchor)", + @"CREATE INDEX idx_metaData_localID ON metaData (localID)", + @"CREATE INDEX idx_metaData_fileID ON metaData (fileID)", + @"CREATE INDEX idx_metaData_removed ON metaData (removed)", + ] + openStatements:@[ + // Create trigger to delete thumbnails alongside metadata entries + @"CREATE TEMPORARY TRIGGER temp_delete_associated_thumbnails AFTER DELETE ON metaData BEGIN DELETE FROM thumb.thumbnails WHERE fileID = OLD.fileID; END" // relatedTo:OCDatabaseTableNameThumbnails + ] + upgradeMigrator:^(OCSQLiteDB *db, OCSQLiteTableSchema *schema, void (^completionHandler)(NSError *error)) { + // Migrate to version 5 + + [db executeTransaction:[OCSQLiteTransaction transactionWithBlock:^NSError *(OCSQLiteDB *db, OCSQLiteTransaction *transaction) { + INSTALL_TRANSACTION_ERROR_COLLECTION_RESULT_HANDLER + + // Add "removed" column + [db executeQuery:[OCSQLiteQuery query:@"ALTER TABLE metaData ADD COLUMN localID TEXT" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Create "localID" index + [db executeQuery:[OCSQLiteQuery query:@"CREATE INDEX idx_metaData_localID ON metaData (localID)" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Create "fileID" index + [db executeQuery:[OCSQLiteQuery query:@"CREATE INDEX idx_metaData_fileID ON metaData (fileID)" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Migrate + [db executeQuery:[OCSQLiteQuery querySelectingColumns:@[@"mdID", @"itemData"] fromTable:OCDatabaseTableNameMetaData where:nil resultHandler:^(OCSQLiteDB *db, NSError *error, OCSQLiteTransaction *transaction, OCSQLiteResultSet *resultSet) { + // Migrate OCItems + [resultSet iterateUsing:^(OCSQLiteResultSet *resultSet, NSUInteger line, NSDictionary *rowDictionary, BOOL *stop) { + OCItem *item; + + if ((item = [OCItem itemFromSerializedData:rowDictionary[@"itemData"]]) != nil) + { + if (rowDictionary[@"mdID"] != nil) + { + item.localID = item.fileID; + + if (item.parentFileID != nil) + { + item.parentLocalID = item.parentFileID; + } + + [db executeQuery:[OCSQLiteQuery queryUpdatingRowWithID:rowDictionary[@"mdID"] + inTable:OCDatabaseTableNameMetaData + withRowValues:@{ + @"localID" : item.localID, + @"itemData" : [item serializedData] + } + completionHandler:^(OCSQLiteDB *db, NSError *error) { + if (error != nil) + { + transactionError = error; + } + } + ] + ]; + } + } + } error:&transactionError]; + }]]; + if (transactionError != nil) { return(transactionError); } + + return (transactionError); + + } type:OCSQLiteTransactionTypeDeferred completionHandler:^(OCSQLiteDB *db, OCSQLiteTransaction *transaction, NSError *error) { + completionHandler(error); + }]]; + }] + ]; + + // Version 6 + [self.sqlDB addTableSchema:[OCSQLiteTableSchema + schemaWithTableName:OCDatabaseTableNameMetaData + version:6 + creationQueries:@[ + /* + mdID : INTEGER - unique ID used to uniquely identify and efficiently update a row + type : INTEGER - OCItemType value to indicate if this is a file or a collection/folder + syncAnchor: INTEGER - sync anchor, a number that increases its value with every change to an entry. For files, higher sync anchor values indicate the file changed (incl. creation, content or meta data changes). For collections/folders, higher sync anchor values indicate the list of items in the collection/folder changed in a way not covered by file entries (i.e. rename, deletion, but not creation of files). + removed : INTEGER - value indicating if this file or folder has been removed: 1 if it was, 0 if not (default). Removed entries are kept around until their delta to the latest syncAnchor value exceeds -[OCDatabase removedItemRetentionLength]. + locallyModified: INTEGER- value indicating if this is a file that's been created or modified locally + localRelativePath: TEXT - path of the local copy of the item, relative to the rootURL of the vault that stores it + path : TEXT - full path of the item (e.g. "/example/file.txt") + parentPath : TEXT - parent path of the item. (e.g. "/example" for an item at "/example/file.txt") + name : TEXT - name of the item (e.g. "file.txt" for an item at "/example/file.txt") + fileID : TEXT - OCFileID identifying the item + localID : TEXT - OCLocalID identifying the item + itemData : BLOB - data of the serialized OCItem + */ + @"CREATE TABLE metaData (mdID INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER NOT NULL, syncAnchor INTEGER NOT NULL, removed INTEGER NOT NULL, locallyModified INTEGER NOT NULL, localRelativePath TEXT NULL, path TEXT NOT NULL, parentPath TEXT NOT NULL, name TEXT NOT NULL, fileID TEXT NOT NULL, localID TEXT, itemData BLOB NOT NULL)", + + // Create indexes over path and parentPath + @"CREATE INDEX idx_metaData_path ON metaData (path)", + @"CREATE INDEX idx_metaData_parentPath ON metaData (parentPath)", + @"CREATE INDEX idx_metaData_synchAnchor ON metaData (syncAnchor)", + @"CREATE INDEX idx_metaData_localID ON metaData (localID)", + @"CREATE INDEX idx_metaData_fileID ON metaData (fileID)", + @"CREATE INDEX idx_metaData_removed ON metaData (removed)", + ] + openStatements:@[ + // Create trigger to delete thumbnails alongside metadata entries + @"CREATE TEMPORARY TRIGGER temp_delete_associated_thumbnails AFTER DELETE ON metaData BEGIN DELETE FROM thumb.thumbnails WHERE fileID = OLD.fileID; END" // relatedTo:OCDatabaseTableNameThumbnails + ] + upgradeMigrator:^(OCSQLiteDB *db, OCSQLiteTableSchema *schema, void (^completionHandler)(NSError *error)) { + // Migrate to version 4 + + [db executeTransaction:[OCSQLiteTransaction transactionWithBlock:^NSError *(OCSQLiteDB *db, OCSQLiteTransaction *transaction) { + INSTALL_TRANSACTION_ERROR_COLLECTION_RESULT_HANDLER + + // Create new table + [db executeQuery:[OCSQLiteQuery query:@"CREATE TABLE metaData_new (mdID INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER NOT NULL, syncAnchor INTEGER NOT NULL, removed INTEGER NOT NULL, locallyModified INTEGER NOT NULL, localRelativePath TEXT NULL, path TEXT NOT NULL, parentPath TEXT NOT NULL, name TEXT NOT NULL, fileID TEXT NOT NULL, localID TEXT, itemData BLOB NOT NULL)" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Migrate data to new table + [db executeQuery:[OCSQLiteQuery query:@"INSERT INTO metaData_new (mdID, type, syncAnchor, removed, locallyModified, localRelativePath, path, parentPath, name, fileID, localID, itemData) SELECT mdID, type, syncAnchor, removed, locallyModified, localRelativePath, path, parentPath, name, fileID, localID, itemData FROM metaData" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Drop old table + [db executeQuery:[OCSQLiteQuery query:@"DROP TABLE metaData" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Rename new table + [db executeQuery:[OCSQLiteQuery query:@"ALTER TABLE metaData_new RENAME TO metaData" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + return (transactionError); + } type:OCSQLiteTransactionTypeDeferred completionHandler:^(OCSQLiteDB *db, OCSQLiteTransaction *transaction, NSError *error) { + completionHandler(error); + }]]; + }] + ]; } - (void)addOrUpdateSyncJournalSchema @@ -320,6 +478,91 @@ - (void)addOrUpdateSyncJournalSchema }]]; }] ]; + + // Version 3 + [self.sqlDB addTableSchema:[OCSQLiteTableSchema + schemaWithTableName:OCDatabaseTableNameSyncJournal + version:3 + creationQueries:@[ + /* + recordID : INTEGER - unique ID used to uniquely identify and efficiently update a row + timestampDate : REAL - NSDate.timeIntervalSinceReferenceDate at the time the record was added to the journal + inProgressSinceDate : REAL - NSDate.timeIntervalSinceReferenceDate at the time the record was beginning to be processed + action : TEXT - action to perform + localID : TEXT - localID of the item targeted by the operation + path : TEXT - path of the item targeted by the operation + recordData : BLOB - archived OCSyncRecord data + */ + @"CREATE TABLE syncJournal (recordID INTEGER PRIMARY KEY, timestampDate REAL NOT NULL, inProgressSinceDate REAL, action TEXT NOT NULL, localID TEXT NOT NULL, path TEXT NOT NULL, recordData BLOB)", + ] + openStatements:nil + upgradeMigrator:^(OCSQLiteDB *db, OCSQLiteTableSchema *schema, void (^completionHandler)(NSError *error)) { + // Migrate to version 3 + [db executeTransaction:[OCSQLiteTransaction transactionWithBlock:^NSError *(OCSQLiteDB *db, OCSQLiteTransaction *transaction) { + INSTALL_TRANSACTION_ERROR_COLLECTION_RESULT_HANDLER + + // Drop previous table + [db executeQuery:[OCSQLiteQuery query:@"DROP TABLE syncJournal" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Create it anew + [db executeQuery:[OCSQLiteQuery query:@"CREATE TABLE syncJournal (recordID INTEGER PRIMARY KEY, timestampDate REAL NOT NULL, inProgressSinceDate REAL, action TEXT NOT NULL, localID TEXT NOT NULL, path TEXT NOT NULL, recordData BLOB)" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + return (transactionError); + + } type:OCSQLiteTransactionTypeDeferred completionHandler:^(OCSQLiteDB *db, OCSQLiteTransaction *transaction, NSError *error) { + completionHandler(error); + }]]; + }] + ]; + + // Version 4 + [self.sqlDB addTableSchema:[OCSQLiteTableSchema + schemaWithTableName:OCDatabaseTableNameSyncJournal + version:4 + creationQueries:@[ + /* + recordID : INTEGER - unique ID used to uniquely identify and efficiently update a row + timestampDate : REAL - NSDate.timeIntervalSinceReferenceDate at the time the record was added to the journal + inProgressSinceDate : REAL - NSDate.timeIntervalSinceReferenceDate at the time the record was beginning to be processed + action : TEXT - action to perform + localID : TEXT - localID of the item targeted by the operation + path : TEXT - path of the item targeted by the operation + recordData : BLOB - archived OCSyncRecord data + */ + @"CREATE TABLE syncJournal (recordID INTEGER PRIMARY KEY AUTOINCREMENT, timestampDate REAL NOT NULL, inProgressSinceDate REAL, action TEXT NOT NULL, localID TEXT NOT NULL, path TEXT NOT NULL, recordData BLOB)", + ] + openStatements:nil + upgradeMigrator:^(OCSQLiteDB *db, OCSQLiteTableSchema *schema, void (^completionHandler)(NSError *error)) { + // Migrate to version 4 + [db executeTransaction:[OCSQLiteTransaction transactionWithBlock:^NSError *(OCSQLiteDB *db, OCSQLiteTransaction *transaction) { + INSTALL_TRANSACTION_ERROR_COLLECTION_RESULT_HANDLER + + // Create new table + [db executeQuery:[OCSQLiteQuery query:@"CREATE TABLE syncJournal_new (recordID INTEGER PRIMARY KEY AUTOINCREMENT, timestampDate REAL NOT NULL, inProgressSinceDate REAL, action TEXT NOT NULL, localID TEXT NOT NULL, path TEXT NOT NULL, recordData BLOB)" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Migrate data to new table + [db executeQuery:[OCSQLiteQuery query:@"INSERT INTO syncJournal_new (recordID, timestampDate, inProgressSinceDate, action, localID, path, recordData) SELECT recordID, timestampDate, inProgressSinceDate, action, localID, path, recordData FROM syncJournal" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Drop old table + [db executeQuery:[OCSQLiteQuery query:@"DROP TABLE syncJournal" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Rename new table + [db executeQuery:[OCSQLiteQuery query:@"ALTER TABLE syncJournal_new RENAME TO syncJournal" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + return (transactionError); + + } type:OCSQLiteTransactionTypeDeferred completionHandler:^(OCSQLiteDB *db, OCSQLiteTransaction *transaction, NSError *error) { + completionHandler(error); + }]]; + }] + ]; + } - (void)addOrUpdateEvents @@ -371,6 +614,49 @@ - (void)addOrUpdateEvents }]]; }] ]; + + // Version 3 + [self.sqlDB addTableSchema:[OCSQLiteTableSchema + schemaWithTableName:OCDatabaseTableNameEvents + version:2 + creationQueries:@[ + /* + eventID : INTEGER - unique ID used to uniquely identify and efficiently update a row + recordID : INTEGER - ID of sync record this event refers to + processSession : BLOB - process session the event was added from + eventData : BLOB - archived OCEvent data + */ + @"CREATE TABLE events (eventID INTEGER PRIMARY KEY AUTOINCREMENT, recordID INTEGER NOT NULL, processSession BLOB NOT NULL, eventData BLOB NOT NULL)", + ] + openStatements:nil + upgradeMigrator:^(OCSQLiteDB *db, OCSQLiteTableSchema *schema, void (^completionHandler)(NSError *error)) { + // Migrate to version 2 + [db executeTransaction:[OCSQLiteTransaction transactionWithBlock:^NSError *(OCSQLiteDB *db, OCSQLiteTransaction *transaction) { + INSTALL_TRANSACTION_ERROR_COLLECTION_RESULT_HANDLER + + // Create new table + [db executeQuery:[OCSQLiteQuery query:@"CREATE TABLE events_new (eventID INTEGER PRIMARY KEY AUTOINCREMENT, recordID INTEGER NOT NULL, processSession BLOB NOT NULL, eventData BLOB NOT NULL)" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Migrate data to new table + [db executeQuery:[OCSQLiteQuery query:@"INSERT INTO events_new (eventID, recordID, processSession, eventData) SELECT eventID, recordID, processSession, eventData FROM events" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Drop old table + [db executeQuery:[OCSQLiteQuery query:@"DROP TABLE events" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + // Rename new table + [db executeQuery:[OCSQLiteQuery query:@"ALTER TABLE events_new RENAME TO events" resultHandler:resultHandler]]; + if (transactionError != nil) { return(transactionError); } + + return (transactionError); + + } type:OCSQLiteTransactionTypeDeferred completionHandler:^(OCSQLiteDB *db, OCSQLiteTransaction *transaction, NSError *error) { + completionHandler(error); + }]]; + }] + ]; } - (void)addOrUpdateThumbnailsSchema @@ -415,7 +701,7 @@ - (void)addOrUpdateThumbnailsSchema mimeType : TEXT - MIME Type of imageData imageData : BLOB - image data of the thumbnail */ - @"CREATE TABLE thumb.thumbnails (tnID INTEGER PRIMARY KEY, fileID TEXT NOT NULL, eTag TEXT NOT NULL, specID TEXT NOT NULL, maxWidth INTEGER NOT NULL, maxHeight INTEGER NOT NULL, mimeType TEXT NOT NULL, imageData BLOB NOT NULL)", // relatedTo:OCDatabaseTableNameThumbnails + @"CREATE TABLE thumb.thumbnails (tnID INTEGER PRIMARY KEY AUTOINCREMENT, fileID TEXT NOT NULL, eTag TEXT NOT NULL, specID TEXT NOT NULL, maxWidth INTEGER NOT NULL, maxHeight INTEGER NOT NULL, mimeType TEXT NOT NULL, imageData BLOB NOT NULL)", // relatedTo:OCDatabaseTableNameThumbnails // Create index over fileID @"CREATE INDEX thumb.idx_thumbnails_fileID ON thumbnails (fileID)" // relatedTo:OCDatabaseTableNameThumbnails @@ -455,7 +741,7 @@ - (void)addOrUpdateConnectionRequestsSchema taskID : INTEGER - URL Session Task ID of the request when scheduled, NULL otherwise (optional) requestData : BLOB - data of the serialized OCConnectionRequest */ - @"CREATE TABLE requests (rqID INTEGER PRIMARY KEY, jobID TEXT, groupID TEXT, urlSessionID TEXT NOT NULL, taskID INTEGER, requestData BLOB NOT NULL)" + @"CREATE TABLE requests (rqID INTEGER PRIMARY KEY AUTOINCREMENT, jobID TEXT, groupID TEXT, urlSessionID TEXT NOT NULL, taskID INTEGER, requestData BLOB NOT NULL)" ] openStatements:nil upgradeMigrator:nil] @@ -477,7 +763,7 @@ - (void)addOrUpdateCountersSchema value : INTEGER - Current value of the counter lastUpdated : REAL - NSDate.timeIntervalSinceReferenceDate for when the counter was last updated */ - @"CREATE TABLE counters (cnID INTEGER PRIMARY KEY, identifier TEXT NOT NULL, value INTEGER NOT NULL, lastUpdated REAL NOT NULL)" // relatedTo:OCDatabaseTableNameCounters + @"CREATE TABLE counters (cnID INTEGER PRIMARY KEY AUTOINCREMENT, identifier TEXT NOT NULL, value INTEGER NOT NULL, lastUpdated REAL NOT NULL)" // relatedTo:OCDatabaseTableNameCounters ] openStatements:nil upgradeMigrator:nil] diff --git a/ownCloudSDK/Vaults/Database/OCDatabase.h b/ownCloudSDK/Vaults/Database/OCDatabase.h index 042d15b5..6370d217 100644 --- a/ownCloudSDK/Vaults/Database/OCDatabase.h +++ b/ownCloudSDK/Vaults/Database/OCDatabase.h @@ -39,6 +39,8 @@ typedef void(^OCDatabaseRetrieveSyncRecordCompletionHandler)(OCDatabase *db, NSE typedef void(^OCDatabaseRetrieveSyncRecordsCompletionHandler)(OCDatabase *db, NSError *error, NSArray *syncRecords); typedef void(^OCDatabaseProtectedBlockCompletionHandler)(NSError *error, NSNumber *previousCounterValue, NSNumber *newCounterValue); +typedef NSArray *(^OCDatabaseItemFilter)(NSArray *items); + typedef NSString* OCDatabaseTableName NS_TYPED_ENUM; typedef NSString* OCDatabaseCounterIdentifier; @@ -52,6 +54,8 @@ typedef NSString* OCDatabaseCounterIdentifier; NSUInteger _removedItemRetentionLength; + OCDatabaseItemFilter _itemFilter; + OCSQLiteDB *_sqlDB; } @@ -59,6 +63,8 @@ typedef NSString* OCDatabaseCounterIdentifier; @property(assign) NSUInteger removedItemRetentionLength; +@property(copy) OCDatabaseItemFilter itemFilter; + @property(strong) OCSQLiteDB *sqlDB; #pragma mark - Initialization @@ -76,7 +82,10 @@ typedef NSString* OCDatabaseCounterIdentifier; - (void)updateCacheItems:(NSArray *)items syncAnchor:(OCSyncAnchor)syncAnchor completionHandler:(OCDatabaseCompletionHandler)completionHandler; - (void)removeCacheItems:(NSArray *)items syncAnchor:(OCSyncAnchor)syncAnchor completionHandler:(OCDatabaseCompletionHandler)completionHandler; +- (void)retrieveCacheItemForLocalID:(OCLocalID)localID completionHandler:(OCDatabaseRetrieveItemCompletionHandler)completionHandler; + - (void)retrieveCacheItemForFileID:(OCFileID)fileID completionHandler:(OCDatabaseRetrieveItemCompletionHandler)completionHandler; +- (void)retrieveCacheItemForFileID:(OCFileID)fileID includingRemoved:(BOOL)includingRemoved completionHandler:(OCDatabaseRetrieveItemCompletionHandler)completionHandler; - (void)retrieveCacheItemsAtPath:(OCPath)path itemOnly:(BOOL)itemOnly completionHandler:(OCDatabaseRetrieveCompletionHandler)completionHandler; - (NSArray *)retrieveCacheItemsSyncAtPath:(OCPath)path itemOnly:(BOOL)itemOnly error:(NSError * __autoreleasing *)outError syncAnchor:(OCSyncAnchor __autoreleasing *)outSyncAnchor; diff --git a/ownCloudSDK/Vaults/Database/OCDatabase.m b/ownCloudSDK/Vaults/Database/OCDatabase.m index f69fb1d9..b9e4e432 100644 --- a/ownCloudSDK/Vaults/Database/OCDatabase.m +++ b/ownCloudSDK/Vaults/Database/OCDatabase.m @@ -45,6 +45,8 @@ @implementation OCDatabase @synthesize removedItemRetentionLength = _removedItemRetentionLength; +@synthesize itemFilter = _itemFilter; + @synthesize sqlDB = _sqlDB; #pragma mark - Initialization @@ -149,8 +151,23 @@ - (void)addCacheItems:(NSArray *)items syncAnchor:(OCSyncAnchor)syncA { NSMutableArray *queries = [[NSMutableArray alloc] initWithCapacity:items.count]; + if (_itemFilter != nil) + { + items = _itemFilter(items); + } + for (OCItem *item in items) { + if (item.localID == nil) + { + OCLogDebug(@"Item added without localID: %@", item); + } + + if ((item.parentLocalID == nil) && (![item.path isEqualToString:@"/"])) + { + OCLogDebug(@"Item added without parentLocalID: %@", item); + } + [queries addObject:[OCSQLiteQuery queryInsertingIntoTable:OCDatabaseTableNameMetaData rowValues:@{ @"type" : @(item.type), @"syncAnchor" : syncAnchor, @@ -161,6 +178,7 @@ - (void)addCacheItems:(NSArray *)items syncAnchor:(OCSyncAnchor)syncA @"parentPath" : [item.path parentPath], @"name" : [item.path lastPathComponent], @"fileID" : item.fileID, + @"localID" : ((item.localID!=nil) ? item.localID : [NSNull null]), @"itemData" : [item serializedData] } resultHandler:^(OCSQLiteDB *db, NSError *error, NSNumber *rowID) { item.databaseID = rowID; @@ -176,8 +194,23 @@ - (void)updateCacheItems:(NSArray *)items syncAnchor:(OCSyncAnchor)sy { NSMutableArray *queries = [[NSMutableArray alloc] initWithCapacity:items.count]; + if (_itemFilter != nil) + { + items = _itemFilter(items); + } + for (OCItem *item in items) { + if ((item.localID == nil) && (!item.removed)) + { + OCLogDebug(@"Item updated without localID: %@", item); + } + + if ((item.parentLocalID == nil) && (![item.path isEqualToString:@"/"])) + { + OCLogDebug(@"Item updated without parentLocalID: %@", item); + } + if (item.databaseID != nil) { [queries addObject:[OCSQLiteQuery queryUpdatingRowWithID:item.databaseID inTable:OCDatabaseTableNameMetaData withRowValues:@{ @@ -190,6 +223,7 @@ - (void)updateCacheItems:(NSArray *)items syncAnchor:(OCSyncAnchor)sy @"parentPath" : [item.path parentPath], @"name" : [item.path lastPathComponent], @"fileID" : item.fileID, + @"localID" : ((item.localID!=nil) ? item.localID : [NSNull null]), @"itemData" : [item serializedData] } completionHandler:nil]]; } @@ -208,6 +242,11 @@ - (void)removeCacheItems:(NSArray *)items syncAnchor:(OCSyncAnchor)sy { // TODO: Update parent directories with new sync anchor value (not sure if necessary, as a change in eTag should also trigger an update of the parent directory sync anchor) + if (_itemFilter != nil) + { + items = _itemFilter(items); + } + for (OCItem *item in items) { item.removed = YES; @@ -275,7 +314,36 @@ - (void)_completeRetrievalWithResultSet:(OCSQLiteResultSet *)resultSet completio } } +- (void)retrieveCacheItemForLocalID:(OCLocalID)localID completionHandler:(OCDatabaseRetrieveItemCompletionHandler)completionHandler +{ + if (localID == nil) + { + OCLogError(@"Retrieval of localID==nil failed"); + + completionHandler(self, OCError(OCErrorItemNotFound), nil, nil); + return; + } + + [self.sqlDB executeQuery:[OCSQLiteQuery query:@"SELECT mdID, syncAnchor, itemData FROM metaData WHERE localID=? AND removed=0" withParameters:@[localID] resultHandler:^(OCSQLiteDB *db, NSError *error, OCSQLiteTransaction *transaction, OCSQLiteResultSet *resultSet) { + if (error != nil) + { + completionHandler(self, error, nil, nil); + } + else + { + [self _completeRetrievalWithResultSet:resultSet completionHandler:^(OCDatabase *db, NSError *error, OCSyncAnchor syncAnchor, NSArray *items) { + completionHandler(db, error, syncAnchor, items.firstObject); + }]; + } + }]]; +} + - (void)retrieveCacheItemForFileID:(OCFileID)fileID completionHandler:(OCDatabaseRetrieveItemCompletionHandler)completionHandler +{ + [self retrieveCacheItemForFileID:fileID includingRemoved:NO completionHandler:completionHandler]; +} + +- (void)retrieveCacheItemForFileID:(OCFileID)fileID includingRemoved:(BOOL)includingRemoved completionHandler:(OCDatabaseRetrieveItemCompletionHandler)completionHandler { if (fileID == nil) { @@ -285,7 +353,7 @@ - (void)retrieveCacheItemForFileID:(OCFileID)fileID completionHandler:(OCDatabas return; } - [self.sqlDB executeQuery:[OCSQLiteQuery query:@"SELECT mdID, syncAnchor, itemData FROM metaData WHERE fileID=? AND removed=0" withParameters:@[fileID] resultHandler:^(OCSQLiteDB *db, NSError *error, OCSQLiteTransaction *transaction, OCSQLiteResultSet *resultSet) { + [self.sqlDB executeQuery:[OCSQLiteQuery query:(includingRemoved ? @"SELECT mdID, syncAnchor, itemData FROM metaData WHERE fileID=?" : @"SELECT mdID, syncAnchor, itemData FROM metaData WHERE fileID=? AND removed=0") withParameters:@[fileID] resultHandler:^(OCSQLiteDB *db, NSError *error, OCSQLiteTransaction *transaction, OCSQLiteResultSet *resultSet) { if (error != nil) { completionHandler(self, error, nil, nil); @@ -526,6 +594,7 @@ - (void)addSyncRecords:(NSArray *)syncRecords completionHandler @"inProgressSinceDate" : ((syncRecord.inProgressSince != nil) ? syncRecord.inProgressSince : [NSNull null]), @"action" : syncRecord.actionIdentifier, @"path" : path, + @"localID" : syncRecord.localID, @"recordData" : [syncRecord serializedData] } resultHandler:^(OCSQLiteDB *db, NSError *error, NSNumber *rowID) { syncRecord.recordID = rowID; @@ -572,7 +641,8 @@ - (void)updateSyncRecords:(NSArray *)syncRecords completionHand { [queries addObject:[OCSQLiteQuery queryUpdatingRowWithID:syncRecord.recordID inTable:OCDatabaseTableNameSyncJournal withRowValues:@{ @"inProgressSinceDate" : ((syncRecord.inProgressSince != nil) ? syncRecord.inProgressSince : [NSNull null]), - @"recordData" : [syncRecord serializedData] + @"recordData" : [syncRecord serializedData], + @"localID" : syncRecord.localID } completionHandler:^(OCSQLiteDB *db, NSError *error) { @synchronized(db) { diff --git a/ownCloudSDK/Vaults/OCVault.m b/ownCloudSDK/Vaults/OCVault.m index 5f3c96c0..6a072180 100644 --- a/ownCloudSDK/Vaults/OCVault.m +++ b/ownCloudSDK/Vaults/OCVault.m @@ -221,14 +221,14 @@ - (BOOL)fileManager:(NSFileManager *)fileManager shouldRemoveItemAtURL:(NSURL *) #pragma mark - URL and path builders - (NSURL *)localURLForItem:(OCItem *)item { - // Build the URL to where an item should be stored. Follow // pattern. + // Build the URL to where an item should be stored. Follow // pattern. return ([self.filesRootURL URLByAppendingPathComponent:[self relativePathForItem:item] isDirectory:NO]); } - (NSString *)relativePathForItem:(OCItem *)item { - // Build the URL to where an item should be stored. Follow // pattern. - return ([item.fileID stringByAppendingPathComponent:item.name]); + // Build the URL to where an item should be stored. Follow // pattern. + return ([item.localID stringByAppendingPathComponent:item.name]); } + (NSString *)rootPathRelativeToGroupContainerForVaultUUID:(NSUUID *)uuid diff --git a/ownCloudSDK/ownCloudSDK.h b/ownCloudSDK/ownCloudSDK.h index 0acb824c..af03cc7a 100644 --- a/ownCloudSDK/ownCloudSDK.h +++ b/ownCloudSDK/ownCloudSDK.h @@ -88,6 +88,10 @@ FOUNDATION_EXPORT const unsigned char ownCloudSDKVersionString[]; #import #import +#import +#import +#import + #import #import diff --git a/ownCloudSDKTests/CoreSyncTests.m b/ownCloudSDKTests/CoreSyncTests.m index b8aec45e..79b86e6e 100644 --- a/ownCloudSDKTests/CoreSyncTests.m +++ b/ownCloudSDKTests/CoreSyncTests.m @@ -12,6 +12,7 @@ #import "OCCore+Internal.h" #import "TestTools.h" #import "OCTestTarget.h" +#import "OCItem+OCItemCreationDebugging.h" @interface CoreSyncTests : XCTestCase @@ -19,6 +20,16 @@ @interface CoreSyncTests : XCTestCase @implementation CoreSyncTests +- (void)setUp +{ + OCItem.creationHistoryEnabled = YES; +} + +- (void)tearDown +{ + OCItem.creationHistoryEnabled = NO; +} + - (void)dumpMetaDataTableFromCore:(OCCore *)core withDescription:(NSString *)description rowHook:(void(^)(NSUInteger line, NSDictionary> *rowDictionary))rowHook completionHandler:(dispatch_block_t)completionHandler { [core.vault.database.sqlDB executeQuery:[OCSQLiteQuery query:[@"SELECT * FROM " stringByAppendingString:OCDatabaseTableNameMetaData] resultHandler:^(OCSQLiteDB *db, NSError *error, OCSQLiteTransaction *transaction, OCSQLiteResultSet *resultSet) { @@ -393,6 +404,7 @@ - (void)testDelete XCTestExpectation *dirCreationObservedExpectation = [self expectationWithDescription:@"Directory creation observed"]; XCTestExpectation *dirDeletionObservedExpectation = [self expectationWithDescription:@"Directory deletion observed"]; NSString *folderName = NSUUID.UUID.UUIDString; + __block OCLocalID localIDOnCreation=nil, localIDBeforeAction=nil, localIDAfterAction=nil; __block BOOL _dirCreationObserved = NO; // Create bookmark for demo.owncloud.org @@ -411,6 +423,8 @@ - (void)testDelete XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; + core.database.itemFilter = self.databaseSanityCheckFilter; + OCLog(@"Vault location: %@", core.vault.rootURL); query = [OCQuery queryForPath:@"/"]; @@ -454,12 +468,16 @@ - (void)testDelete XCTAssert(error==nil); XCTAssert(item!=nil); + localIDBeforeAction = item.localID; + [core deleteItem:item requireMatch:YES resultHandler:^(NSError *error, OCCore *core, OCItem *item, id parameter) { OCLog(@"------> Delete item result: error=%@ item=%@ parameter=%@", error, item, parameter); XCTAssert(error==nil); XCTAssert(item!=nil); + localIDAfterAction = item.localID; + [dirDeletedExpectation fulfill]; }]; @@ -474,6 +492,8 @@ - (void)testDelete { if ([item.name isEqualToString:folderName]) { + localIDOnCreation = item.localID; + [dirCreationObservedExpectation fulfill]; _dirCreationObserved = YES; } @@ -513,6 +533,12 @@ - (void)testDelete [self waitForExpectationsWithTimeout:60 handler:nil]; + XCTAssert(localIDOnCreation!=nil); + XCTAssert(localIDBeforeAction!=nil); + XCTAssert(localIDAfterAction!=nil); + XCTAssert([localIDOnCreation isEqual:localIDBeforeAction]); + XCTAssert([localIDOnCreation isEqual:localIDAfterAction]); + // Erase vault [core.vault eraseSyncWithCompletionHandler:^(id sender, NSError *error) { XCTAssert((error==nil), @"Erased with error: %@", error); @@ -530,6 +556,7 @@ - (void)testCreateFolder XCTestExpectation *dirCreationObservedExpectation = [self expectationWithDescription:@"Directory creation observed"]; XCTestExpectation *dirPlaceholderCreationObservedExpectation = [self expectationWithDescription:@"Directory placeholder creation observed"]; NSString *folderName = NSUUID.UUID.UUIDString; + __block OCLocalID localIDOnQueryPlaceholder = nil, localIDOnQueryActual=nil, localIDOnCreation=nil; __block BOOL _dirCreationObserved = NO, _dirCreationPlaceholderObserved = NO; // Create bookmark for demo.owncloud.org @@ -548,6 +575,8 @@ - (void)testCreateFolder XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; + core.database.itemFilter = self.databaseSanityCheckFilter; + OCLog(@"Vault location: %@", core.vault.rootURL); query = [OCQuery queryForPath:@"/"]; @@ -591,6 +620,8 @@ - (void)testCreateFolder XCTAssert(error==nil); XCTAssert(item!=nil); + localIDOnCreation = item.localID; + [dirCreatedExpectation fulfill]; }]; } @@ -606,12 +637,16 @@ - (void)testCreateFolder { if (!_dirCreationPlaceholderObserved) { + localIDOnQueryPlaceholder = item.localID; + [dirPlaceholderCreationObservedExpectation fulfill]; _dirCreationPlaceholderObserved = YES; } } else { + localIDOnQueryActual = item.localID; + [dirCreationObservedExpectation fulfill]; _dirCreationObserved = YES; } @@ -638,6 +673,12 @@ - (void)testCreateFolder [self waitForExpectationsWithTimeout:60 handler:nil]; + XCTAssert(localIDOnCreation!=nil); + XCTAssert(localIDOnQueryPlaceholder!=nil); + XCTAssert(localIDOnQueryActual!=nil); + XCTAssert([localIDOnCreation isEqual:localIDOnQueryPlaceholder]); + XCTAssert([localIDOnCreation isEqual:localIDOnQueryActual]); + // Erase vault [core.vault eraseSyncWithCompletionHandler:^(id sender, NSError *error) { XCTAssert((error==nil), @"Erased with error: %@", error); @@ -659,6 +700,9 @@ - (void)testCopy __block XCTestExpectation *targetRemovedStateChangeExpectation = [self expectationWithDescription:@"State changed to target removed"]; __block XCTestExpectation *fileCopiedNotificationExpectation = [self expectationWithDescription:@"File copied notification"]; __block XCTestExpectation *folderCopiedNotificationExpectation = [self expectationWithDescription:@"Folder copied notification"]; + __block OCLocalID localIDQueryPlaceholderCopyFile=nil, localIDCopiedFile=nil, localIDQueryPlaceholderCopyFolder=nil, localIDCopiedFolder=nil; + __block OCLocalID localIDQueryPlaceholderCopyFileParent=nil, localIDCopiedFileParent=nil, localIDQueryPlaceholderCopyFolderParent=nil, localIDCopiedFolderParent=nil; + __block OCLocalID localIDParentFolderPlaceholderOnQuery=nil, localIDParentFolderCompleteOnQuery=nil, localIDParentFolderOnCompletion=nil, localIDParentFolderOnDeleteCompletion=nil; NSString *folderName = NSUUID.UUID.UUIDString; // Create bookmark for demo.owncloud.org @@ -677,6 +721,8 @@ - (void)testCopy XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; + core.database.itemFilter = self.databaseSanityCheckFilter; + OCLog(@"Vault location: %@", core.vault.rootURL); query = [OCQuery queryForPath:@"/"]; @@ -735,6 +781,8 @@ - (void)testCopy XCTAssert(error==nil); XCTAssert(item!=nil); + localIDParentFolderOnCompletion = item.localID; + [dirCreatedExpectation fulfill]; newFolderQuery = [OCQuery queryForPath:item.path]; @@ -774,12 +822,24 @@ - (void)testCopy { if ([item.name isEqualToString:[copyFolderItem.name stringByAppendingString:@" copy"]]) { + if (item.isPlaceholder && (localIDQueryPlaceholderCopyFolder==nil)) + { + localIDQueryPlaceholderCopyFolder = item.localID; + localIDQueryPlaceholderCopyFolderParent = item.parentLocalID; + } + [folderCopiedNotificationExpectation fulfill]; folderCopiedNotificationExpectation = nil; } if ([item.name isEqualToString:[copyFileItem.name stringByAppendingString:@" copy"]]) { + if (item.isPlaceholder && (localIDQueryPlaceholderCopyFile==nil)) + { + localIDQueryPlaceholderCopyFile = item.localID; + localIDQueryPlaceholderCopyFileParent = item.parentLocalID; + } + [fileCopiedNotificationExpectation fulfill]; fileCopiedNotificationExpectation = nil; } @@ -809,6 +869,9 @@ - (void)testCopy XCTAssert([newItem.parentFileID isEqual:item.fileID]); XCTAssert([newItem.name isEqual:[copyFolderItem.name stringByAppendingString:@" copy"]]); + localIDCopiedFolder = newItem.localID; + localIDCopiedFolderParent = newItem.parentLocalID; + [folderCopiedExpectation fulfill]; }]; @@ -820,6 +883,9 @@ - (void)testCopy XCTAssert([newItem.parentFileID isEqual:item.fileID]); XCTAssert([newItem.name isEqual:[copyFileItem.name stringByAppendingString:@" copy"]]); + localIDCopiedFile = newItem.localID; + localIDCopiedFileParent = newItem.parentLocalID; + [fileCopiedExpectation fulfill]; }]; @@ -836,6 +902,8 @@ - (void)testCopy [core deleteItem:item requireMatch:YES resultHandler:^(NSError *error, OCCore *core, OCItem *item, id parameter) { OCLog(@"Delete test folder: error=%@ item=%@", error, item); + localIDParentFolderOnDeleteCompletion = item.localID; + // Stop core [core stopWithCompletionHandler:^(id sender, NSError *error) { XCTAssert((error==nil), @"Stopped with error: %@", error); @@ -846,6 +914,27 @@ - (void)testCopy }]; }]; } + else + { + for (OCItem *item in query.queryResults) + { + if (localIDParentFolderPlaceholderOnQuery == nil) + { + if ([item.name isEqual:folderName] && item.isPlaceholder) + { + localIDParentFolderPlaceholderOnQuery = item.localID; + } + } + + if (localIDParentFolderCompleteOnQuery == nil) + { + if ([item.name isEqual:folderName] && !item.isPlaceholder) + { + localIDParentFolderCompleteOnQuery = item.localID; + } + } + } + } } }]; }; @@ -855,6 +944,18 @@ - (void)testCopy [self waitForExpectationsWithTimeout:60 handler:nil]; + XCTAssert([localIDQueryPlaceholderCopyFile isEqual:localIDCopiedFile]); + XCTAssert([localIDQueryPlaceholderCopyFolder isEqual:localIDCopiedFolder]); + + XCTAssert([localIDQueryPlaceholderCopyFileParent isEqual:localIDCopiedFileParent]); + XCTAssert([localIDQueryPlaceholderCopyFileParent isEqual:localIDQueryPlaceholderCopyFolderParent]); + XCTAssert([localIDQueryPlaceholderCopyFileParent isEqual:localIDCopiedFolderParent]); + + XCTAssert([localIDQueryPlaceholderCopyFileParent isEqual:localIDParentFolderPlaceholderOnQuery]); + XCTAssert([localIDQueryPlaceholderCopyFileParent isEqual:localIDParentFolderCompleteOnQuery]); + XCTAssert([localIDQueryPlaceholderCopyFileParent isEqual:localIDParentFolderOnCompletion]); + XCTAssert([localIDQueryPlaceholderCopyFileParent isEqual:localIDParentFolderOnDeleteCompletion]); + // Erase vault [core.vault eraseSyncWithCompletionHandler:^(id sender, NSError *error) { XCTAssert((error==nil), @"Erased with error: %@", error); @@ -884,7 +985,10 @@ - (void)testMove __block XCTestExpectation *folderReappearedExpectation = [self expectationWithDescription:@"Folder reappeared."]; NSString *folderName = NSUUID.UUID.UUIDString; __block OCItem *moveFolderItem, *moveFileItem; - + __block OCLocalID localIDFileInitial=nil, localIDFileAfterMove=nil, localIDFileMoveBack=nil, localIDFileQueryAfterMove=nil, localIDFileQueryMoveBack=nil; + __block OCLocalID localIDFolderInitial=nil, localIDFolderAfterMove=nil, localIDFolderMoveBack=nil, localIDFolderQueryAfterMove=nil, localIDFolderQueryMoveBack=nil; + __block OCLocalID localIDFileParentInitial=nil, localIDFolderParentInitial=nil, localIDRootInitial=nil, localIDFileParentMoveBack=nil, localIDFolderParentMoveBack=nil; + __block OCLocalID localIDTargetFolderInitial=nil, localIDFileParentAfterMove=nil, localIDFolderParentAfterMove=nil; // Create bookmark for demo.owncloud.org bookmark = [OCBookmark bookmarkForURL:OCTestTarget.secureTargetURL]; @@ -902,6 +1006,8 @@ - (void)testMove XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; + core.database.itemFilter = self.databaseSanityCheckFilter; + OCLog(@"Vault location: %@", core.vault.rootURL); query = [OCQuery queryForPath:@"/"]; @@ -939,16 +1045,22 @@ - (void)testMove { if (!didCreateFolder) { + localIDRootInitial = query.rootItem.localID; + for (OCItem *item in query.queryResults) { if (item.type == OCItemTypeFile) { moveFileItem = item; + localIDFileInitial = item.localID; + localIDFileParentInitial = item.parentLocalID; } if (item.type == OCItemTypeCollection) { moveFolderItem = item; + localIDFolderInitial = item.localID; + localIDFolderParentInitial = item.parentLocalID; } } @@ -960,6 +1072,8 @@ - (void)testMove [dirCreatedExpectation fulfill]; + localIDTargetFolderInitial = item.localID; + newFolderQuery = [OCQuery queryForPath:item.path]; newFolderQuery.changesAvailableNotificationHandler = ^(OCQuery *query) { [query requestChangeSetWithFlags:OCQueryChangeSetRequestFlagDefault completionHandler:^(OCQuery *query, OCQueryChangeSet *changeset) { @@ -999,12 +1113,30 @@ - (void)testMove { [folderCopiedNotificationExpectation fulfill]; folderCopiedNotificationExpectation = nil; + + if (localIDFolderQueryAfterMove == nil) + { + localIDFolderQueryAfterMove = item.localID; + } + else + { + XCTAssert([localIDFolderQueryAfterMove isEqual:item.localID]); + } } if ([item.name isEqualToString:[moveFileItem.name stringByAppendingString:@" moved"]]) { [fileCopiedNotificationExpectation fulfill]; fileCopiedNotificationExpectation = nil; + + if (localIDFileQueryAfterMove == nil) + { + localIDFileQueryAfterMove = item.localID; + } + else + { + XCTAssert([localIDFileQueryAfterMove isEqual:item.localID]); + } } } } @@ -1026,6 +1158,11 @@ - (void)testMove XCTAssert([newItem.parentFileID isEqual:item.fileID]); XCTAssert([newItem.name isEqual:[moveFolderItem.name stringByAppendingString:@" moved"]]); + localIDFolderAfterMove = newItem.localID; + localIDFolderParentAfterMove = newItem.parentLocalID; + + XCTAssert([localIDTargetFolderInitial isEqual:localIDFolderParentAfterMove]); + [folderMovedExpectation fulfill]; [core moveItem:newItem to:query.rootItem withName:moveFolderItem.name options:nil resultHandler:^(NSError *error, OCCore *core, OCItem *newItem, id parameter) { @@ -1036,6 +1173,9 @@ - (void)testMove XCTAssert([newItem.parentFileID isEqual:query.rootItem.fileID]); XCTAssert([newItem.name isEqual:moveFolderItem.name]); + localIDFolderMoveBack = newItem.localID; + localIDFolderParentMoveBack = newItem.parentLocalID; + [folderMovedBackExpectation fulfill]; }]; }]; @@ -1048,6 +1188,11 @@ - (void)testMove XCTAssert([newItem.parentFileID isEqual:item.fileID]); XCTAssert([newItem.name isEqual:[moveFileItem.name stringByAppendingString:@" moved"]]); + localIDFileAfterMove = newItem.localID; + localIDFileParentAfterMove = newItem.parentLocalID; + + XCTAssert([localIDTargetFolderInitial isEqual:localIDFileParentAfterMove]); + [fileMovedExpectation fulfill]; [core moveItem:newItem to:query.rootItem withName:moveFileItem.name options:nil resultHandler:^(NSError *error, OCCore *core, OCItem *newItem, id parameter) { @@ -1058,6 +1203,9 @@ - (void)testMove XCTAssert([newItem.parentFileID isEqual:query.rootItem.fileID]); XCTAssert([newItem.name isEqual:moveFileItem.name]); + localIDFileMoveBack = newItem.localID; + localIDFileParentMoveBack = newItem.parentLocalID; + [fileMovedBackExpectation fulfill]; [core moveItem:newItem to:query.rootItem withName:newItem.name options:nil resultHandler:^(NSError *error, OCCore *core, OCItem *newItem, id parameter) { @@ -1088,17 +1236,20 @@ - (void)testMove else { BOOL hasSeenFolder=NO, hasSeenFile=NO; + OCItem *fileItem = nil, *folderItem = nil; for (OCItem *item in query.queryResults) { if ([item.itemVersionIdentifier isEqual:moveFileItem.itemVersionIdentifier]) { hasSeenFile = YES; + fileItem = item; } if ([item.itemVersionIdentifier isEqual:moveFolderItem.itemVersionIdentifier]) { hasSeenFolder = YES; + folderItem = item; } } @@ -1108,6 +1259,15 @@ - (void)testMove { [fileReappearedExpectation fulfill]; fileReappearedExpectation = nil; + + if (localIDFileQueryMoveBack == nil) + { + localIDFileQueryMoveBack = fileItem.localID; + } + else + { + XCTAssert([localIDFileQueryMoveBack isEqual:fileItem.localID]); + } } } else @@ -1122,6 +1282,15 @@ - (void)testMove { [folderReappearedExpectation fulfill]; folderReappearedExpectation = nil; + + if (localIDFolderQueryMoveBack == nil) + { + localIDFolderQueryMoveBack = folderItem.localID; + } + else + { + XCTAssert([localIDFolderQueryMoveBack isEqual:folderItem.localID]); + } } } else @@ -1139,6 +1308,25 @@ - (void)testMove [self waitForExpectationsWithTimeout:60 handler:nil]; + XCTAssert([localIDFileInitial isEqual:localIDFileAfterMove]); + XCTAssert([localIDFileInitial isEqual:localIDFileMoveBack]); + XCTAssert([localIDFileInitial isEqual:localIDFileQueryAfterMove]); + XCTAssert([localIDFileInitial isEqual:localIDFileQueryMoveBack]); + + XCTAssert([localIDFolderInitial isEqual:localIDFolderAfterMove]); + XCTAssert([localIDFolderInitial isEqual:localIDFolderMoveBack]); + XCTAssert([localIDFolderInitial isEqual:localIDFolderQueryAfterMove]); + XCTAssert([localIDFolderInitial isEqual:localIDFolderQueryMoveBack]); + + XCTAssert([localIDFileParentInitial isEqual:localIDFolderParentInitial]); + XCTAssert([localIDFileParentInitial isEqual:localIDRootInitial]); + XCTAssert([localIDFileParentInitial isEqual:localIDFileParentMoveBack]); + XCTAssert([localIDFileParentInitial isEqual:localIDFolderParentMoveBack]); + + XCTAssert([localIDTargetFolderInitial isEqual:localIDFileParentAfterMove]); + XCTAssert([localIDTargetFolderInitial isEqual:localIDFolderParentAfterMove]); + XCTAssert([localIDFileParentAfterMove isEqual:localIDFolderParentAfterMove]); + // Erase vault [core.vault eraseSyncWithCompletionHandler:^(id sender, NSError *error) { XCTAssert((error==nil), @"Erased with error: %@", error); @@ -1170,6 +1358,8 @@ - (void)testRenameStressTest XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; + core.database.itemFilter = self.databaseSanityCheckFilter; + OCLog(@"Vault location: %@", core.vault.rootURL); query = [OCQuery queryForPath:@"/Photos/"]; @@ -1215,6 +1405,8 @@ - (void)testRenameStressTest NSString *modifiedName1 = [item.name stringByAppendingString:@"1"]; NSString *modifiedName2 = [item.name stringByAppendingString:@"2"]; NSString *modifiedName3 = [item.name stringByAppendingString:@"3"]; + OCLocalID fileLocalID = item.localID; + OCLocalID fileLocalParentID = item.parentLocalID; remainingRenames += 4; @@ -1228,6 +1420,8 @@ - (void)testRenameStressTest XCTAssert(newItem!=nil); XCTAssert([newItem.parentFileID isEqual:query.rootItem.fileID]); XCTAssert([newItem.name isEqual:modifiedName1]); + XCTAssert([fileLocalID isEqual:newItem.localID]); + XCTAssert([fileLocalParentID isEqual:newItem.parentLocalID]); [core renameItem:newItem to:modifiedName2 options:nil resultHandler:^(NSError *error, OCCore *core, OCItem *newItem, id parameter) { OCLog(@"Renamed %@ -> %@: error=%@ item=%@", modifiedName1, newItem.name, error, newItem); @@ -1237,6 +1431,8 @@ - (void)testRenameStressTest XCTAssert(newItem!=nil); XCTAssert([newItem.parentFileID isEqual:query.rootItem.fileID]); XCTAssert([newItem.name isEqual:modifiedName2]); + XCTAssert([fileLocalID isEqual:newItem.localID]); + XCTAssert([fileLocalParentID isEqual:newItem.parentLocalID]); [core renameItem:newItem to:modifiedName3 options:nil resultHandler:^(NSError *error, OCCore *core, OCItem *newItem, id parameter) { OCLog(@"Renamed %@ -> %@: error=%@ item=%@", modifiedName2, newItem.name, error, newItem); @@ -1246,6 +1442,8 @@ - (void)testRenameStressTest XCTAssert(newItem!=nil); XCTAssert([newItem.parentFileID isEqual:query.rootItem.fileID]); XCTAssert([newItem.name isEqual:modifiedName3]); + XCTAssert([fileLocalID isEqual:newItem.localID]); + XCTAssert([fileLocalParentID isEqual:newItem.parentLocalID]); [core renameItem:newItem to:originalName options:nil resultHandler:^(NSError *error, OCCore *core, OCItem *newItem, id parameter) { OCLog(@"Renamed %@ -> %@: error=%@ item=%@", modifiedName3, newItem.name, error, newItem); @@ -1255,6 +1453,8 @@ - (void)testRenameStressTest XCTAssert(newItem!=nil); XCTAssert([newItem.parentFileID isEqual:query.rootItem.fileID]); XCTAssert([newItem.name isEqual:originalName]); + XCTAssert([fileLocalID isEqual:newItem.localID]); + XCTAssert([fileLocalParentID isEqual:newItem.parentLocalID]); if (remainingRenames == 0) { @@ -1300,6 +1500,8 @@ - (void)testDownload XCTestExpectation *coreStartedExpectation = [self expectationWithDescription:@"Core started"]; XCTestExpectation *coreStoppedExpectation = [self expectationWithDescription:@"Core stopped"]; XCTestExpectation *fileDownloadedExpectation = [self expectationWithDescription:@"File downloaded"]; + __block OCLocalID localIDQueryBeforeDownload=nil, localIDAfterDownload=nil, localIDQueryAfterDownload=nil; + __block OCFileID downloadFileID = nil; __block BOOL startedDownload = NO; // Create bookmark for demo.owncloud.org @@ -1318,6 +1520,8 @@ - (void)testDownload XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; + core.database.itemFilter = self.databaseSanityCheckFilter; + OCLog(@"Vault location: %@", core.vault.rootURL); query = [OCQuery queryForPath:@"/"]; @@ -1359,11 +1563,17 @@ - (void)testDownload { startedDownload = YES; + localIDQueryBeforeDownload = item.localID; + + downloadFileID = item.fileID; + [core downloadItem:item options:nil resultHandler:^(NSError *error, OCCore *core, OCItem *item, OCFile *file) { OCLog(@"Downloaded to %@ with error %@", file.url, error); [fileDownloadedExpectation fulfill]; + localIDAfterDownload = item.localID; + XCTAssert(error==nil); XCTAssert(file.url!=nil); @@ -1396,6 +1606,13 @@ - (void)testDownload }]; break; } + else if (downloadFileID != nil) + { + if ([item.fileID isEqual:downloadFileID] && (item.localRelativePath != nil)) + { + localIDQueryAfterDownload = item.localID; + } + } } } }]; @@ -1406,6 +1623,9 @@ - (void)testDownload [self waitForExpectationsWithTimeout:60 handler:nil]; + XCTAssert([localIDQueryBeforeDownload isEqualToString:localIDAfterDownload]); + XCTAssert([localIDQueryBeforeDownload isEqualToString:localIDQueryAfterDownload]); + // Erase vault [core.vault eraseSyncWithCompletionHandler:^(id sender, NSError *error) { XCTAssert((error==nil), @"Erased with error: %@", error); @@ -1425,6 +1645,8 @@ - (void)testUpload NSURL *uploadFileURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"rainbow" withExtension:@"png"]; NSURL *modifiedFileURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"rainbow-crystalized" withExtension:@"png"]; NSString *uploadName = [NSString stringWithFormat:@"rainbow-%f.png", NSDate.timeIntervalSinceReferenceDate]; + __block OCLocalID localIDOnQueryPlaceholder = nil, localIDOnQueryActual=nil; + __block OCLocalID localIDUploadActionPlaceholder=nil, localIDUploadActionCompletion=nil, localIDUpdateActionPlaceholder=nil, localIDUpdateActionCompletion=nil; __block BOOL startedUpload = NO; OCChecksum *(^ComputeChecksumForURL)(NSURL *url) = ^(NSURL *url) { __block OCChecksum *checksum = nil; @@ -1457,6 +1679,8 @@ - (void)testUpload XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; + core.database.itemFilter = self.databaseSanityCheckFilter; + OCLog(@"Vault location: %@", core.vault.rootURL); query = [OCQuery queryForPath:@"/"]; @@ -1506,6 +1730,7 @@ - (void)testUpload OCLog(@"### Placeholder item: %@", item); + localIDUploadActionPlaceholder = item.localID; [placeholderCreatedExpectation fulfill]; } resultHandler:^(NSError *error, OCCore *core, OCItem *item, id parameter) { @@ -1516,6 +1741,8 @@ - (void)testUpload OCLog(@"### Uploaded item: %@", item); + localIDUploadActionCompletion = item.localID; + [fileUploadedExpectation fulfill]; // Test upload by local modification @@ -1526,6 +1753,8 @@ - (void)testUpload OCLog(@"### Update \"placeholder\" item=%@ error=%@", item, error); + localIDUpdateActionPlaceholder = item.localID; + [modificationItemCreatedExpectation fulfill]; } resultHandler:^(NSError *error, OCCore *core, OCItem *item, id parameter) { XCTAssert(error==nil); @@ -1535,6 +1764,8 @@ - (void)testUpload OCLog(@"### Uploaded updated item=%@, error=%@", item, error); + localIDUpdateActionCompletion = item.localID; + [updatedFileUploadedExpectation fulfill]; // Stop core @@ -1546,6 +1777,24 @@ - (void)testUpload }]; }]; } + else + { + for (OCItem *item in changeset.queryResult) + { + if ([item.name isEqualToString:uploadName]) + { + if (item.isPlaceholder && (localIDOnQueryPlaceholder==nil)) + { + localIDOnQueryPlaceholder = item.localID; + } + + if (!item.isPlaceholder && (localIDOnQueryActual==nil)) + { + localIDOnQueryActual = item.localID; + } + } + } + } } } }]; @@ -1556,6 +1805,12 @@ - (void)testUpload [self waitForExpectationsWithTimeout:60 handler:nil]; + XCTAssert([localIDOnQueryActual isEqual:localIDOnQueryPlaceholder]); + XCTAssert([localIDOnQueryActual isEqual:localIDUploadActionPlaceholder]); + XCTAssert([localIDOnQueryActual isEqual:localIDUploadActionCompletion]); + XCTAssert([localIDOnQueryActual isEqual:localIDUpdateActionPlaceholder]); + XCTAssert([localIDOnQueryActual isEqual:localIDUpdateActionCompletion]); + // Erase vault [core.vault eraseSyncWithCompletionHandler:^(id sender, NSError *error) { XCTAssert((error==nil), @"Erased with error: %@", error); @@ -1573,6 +1828,7 @@ - (void)testItemUpdates XCTestExpectation *favoriteUnsetExpectation = [self expectationWithDescription:@"Favorite unset"]; XCTestExpectation *propFindReturnedExpectation = [self expectationWithDescription:@"PROPFIND returned"]; __block BOOL didFavorite = NO; + __block OCLocalID localIDBeforeUpdate=nil, localIDAfterFirstUpdate=nil, localIDAfterSecondUpdate = nil; // Create core with bookmark core = [[OCCore alloc] initWithBookmark:bookmark]; @@ -1585,6 +1841,8 @@ - (void)testItemUpdates XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; + core.database.itemFilter = self.databaseSanityCheckFilter; + OCLog(@"Vault location: %@", core.vault.rootURL); query = [OCQuery queryForPath:@"/"]; @@ -1604,6 +1862,8 @@ - (void)testItemUpdates item.isFavorite = @(YES); + localIDBeforeUpdate = item.localID; + [core updateItem:item properties:propertiesToUpdate options:nil resultHandler:^(NSError *error, OCCore *core, OCItem *item, NSDictionary *statusByPropertyName) { OCLog(@"Update item=%@ result: error=%@, statusByPropertyName=%@", item, error, statusByPropertyName); @@ -1614,6 +1874,8 @@ - (void)testItemUpdates [favoriteSetExpectation fulfill]; + localIDAfterFirstUpdate = item.localID; + [core.connection retrieveItemListAtPath:item.path depth:0 completionHandler:^(NSError *error, NSArray *items) { XCTAssert(items.count == 1); XCTAssert(items.firstObject.isFavorite.boolValue); @@ -1625,6 +1887,8 @@ - (void)testItemUpdates [core updateItem:item properties:propertiesToUpdate options:nil resultHandler:^(NSError *error, OCCore *core, OCItem *item, id parameter) { OCLog(@"Update item=%@ result: error=%@, statusByPropertyName=%@", item, error, statusByPropertyName); + localIDAfterSecondUpdate = item.localID; + for (OCItemPropertyName propertyName in propertiesToUpdate) { XCTAssert(statusByPropertyName[propertyName].isSuccess); @@ -1658,6 +1922,9 @@ - (void)testItemUpdates [self waitForExpectationsWithTimeout:60 handler:nil]; + XCTAssert([localIDBeforeUpdate isEqual:localIDAfterFirstUpdate]); + XCTAssert([localIDBeforeUpdate isEqual:localIDAfterSecondUpdate]); + // Erase vault [core.vault eraseSyncWithCompletionHandler:^(id sender, NSError *error) { XCTAssert((error==nil), @"Erased with error: %@", error); diff --git a/ownCloudSDKTests/CoreTests.m b/ownCloudSDKTests/CoreTests.m index 1d737a05..1fe25b04 100644 --- a/ownCloudSDKTests/CoreTests.m +++ b/ownCloudSDKTests/CoreTests.m @@ -99,6 +99,8 @@ - (void)testSimpleQuery [core startWithCompletionHandler:^(OCCore *core, NSError *error) { OCQuery *query; + core.vault.database.itemFilter = self.databaseSanityCheckFilter; + XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; @@ -141,12 +143,16 @@ - (void)testSimpleQuery XCTAssert(query.rootItem!=nil); // Root item exists XCTAssert(query.rootItem.fileID!=nil); // Root item has fileID XCTAssert(query.rootItem.parentFileID==nil); // Root item has no parentFileID + XCTAssert(query.rootItem.localID!=nil); // Root item has localID + XCTAssert(query.rootItem.parentLocalID==nil); // Root item has no parentLocalID for (OCItem *item in changeset.queryResult) { XCTAssert(item.fileID!=nil); // all items in the result have a file ID XCTAssert(item.parentFileID!=nil); // all items in the result have a parent file ID XCTAssert([item.parentFileID isEqual:query.rootItem.fileID]); // all items in result have the parentFileID of their parent dir + XCTAssert(item.parentLocalID!=nil); // all items in the result have a parent local ID + XCTAssert([item.parentLocalID isEqual:query.rootItem.localID]); // all items in result have the parentLocalID of their parent dir } } } @@ -203,14 +209,19 @@ - (void)testSimpleQuery // Verify parentFileID XCTAssert(query.rootItem!=nil); // Root item exists XCTAssert(query.rootItem.fileID!=nil); // Root item has fileID - XCTAssert(query.rootItem.parentFileID!=nil); // Root item has no parentFileID + XCTAssert(query.rootItem.parentFileID!=nil); // Root item has parentFileID XCTAssert([query.rootItem.parentFileID isEqual:firstQueryRootItem.fileID]); // all items in result have the parentFileID of their parent dir + XCTAssert(query.rootItem.localID!=nil); // Root item has localID + XCTAssert(query.rootItem.parentLocalID!=nil); // Root item has parentLocalID + XCTAssert([query.rootItem.parentLocalID isEqual:firstQueryRootItem.localID]); // all items in result have the parentLocalID of their parent dir for (OCItem *item in changeset.queryResult) { XCTAssert(item.fileID!=nil); // all items in the result have a file ID XCTAssert(item.parentFileID!=nil); // all items in the result have a parent file ID XCTAssert([item.parentFileID isEqual:query.rootItem.fileID]); // all items in result have the parentFileID of their parent dir + XCTAssert(item.parentLocalID!=nil); // all items in the result have a parent local ID + XCTAssert([item.parentLocalID isEqual:query.rootItem.localID]); // all items in result have the parentLocalID of their parent dir } } @@ -266,6 +277,8 @@ - (void)testSimpleQueryWithSimulatedCacheUpdate [core startWithCompletionHandler:^(OCCore *core, NSError *error) { OCQuery *query; + core.vault.database.itemFilter = self.databaseSanityCheckFilter; + XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; @@ -426,6 +439,8 @@ - (void)testOfflineCaching [core startWithCompletionHandler:^(OCCore *core, NSError *error) { OCQuery *query; + core.vault.database.itemFilter = self.databaseSanityCheckFilter; + XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; @@ -608,6 +623,8 @@ - (void)testThumbnailRetrieval [core startWithCompletionHandler:^(OCCore *core, NSError *error) { OCQuery *query; + core.vault.database.itemFilter = self.databaseSanityCheckFilter; + XCTAssert((error==nil), @"Started with error: %@", error); [coreStartedExpectation fulfill]; @@ -787,6 +804,8 @@ - (void)testInvalidLoginData [core startWithCompletionHandler:^(OCCore *core, NSError *error) { OCLog(@"Core: %@ Error: %@", core, error); + core.vault.database.itemFilter = self.databaseSanityCheckFilter; + [coreStartedExpectation fulfill]; }]; @@ -824,6 +843,8 @@ - (void)testOverlappingQueries OCQuery *query = [OCQuery queryForPath:@"/"]; __weak OCCore *weakCore = core; + core.vault.database.itemFilter = self.databaseSanityCheckFilter; + query.changesAvailableNotificationHandler = ^(OCQuery * _Nonnull query) { // Wait for population of cache if ((query.state == OCQueryStateIdle) && (initialPopulationReceivedExpectation!=nil)) @@ -920,6 +941,7 @@ - (void)testQueryFilter [[OCCoreManager sharedCoreManager] requestCoreForBookmark:bookmark completionHandler:^(OCCore * _Nullable core, NSError * _Nullable error) { OCQuery *query = [OCQuery queryForPath:@"/"]; + core.vault.database.itemFilter = self.databaseSanityCheckFilter; core.automaticItemListUpdatesEnabled = NO; query.changesAvailableNotificationHandler = ^(OCQuery * _Nonnull query) { diff --git a/ownCloudSDKTests/TestTools.h b/ownCloudSDKTests/TestTools.h index 408f0d85..324fb50f 100644 --- a/ownCloudSDKTests/TestTools.h +++ b/ownCloudSDKTests/TestTools.h @@ -7,9 +7,16 @@ // #import +#import @interface OCVault (TestTools) - (void)eraseSyncWithCompletionHandler:(OCCompletionHandler)completionHandler; @end + +@interface XCTestCase (LocalIDIntegrity) + +- (OCDatabaseItemFilter)databaseSanityCheckFilter; + +@end diff --git a/ownCloudSDKTests/TestTools.m b/ownCloudSDKTests/TestTools.m index 657a60d0..78877eab 100644 --- a/ownCloudSDKTests/TestTools.m +++ b/ownCloudSDKTests/TestTools.m @@ -8,6 +8,7 @@ #import "TestTools.h" #import "OCMacros.h" +#import "OCItem+OCItemCreationDebugging.h" @implementation OCVault (TestTools) @@ -26,3 +27,80 @@ - (void)eraseSyncWithCompletionHandler:(OCCompletionHandler)completionHandler } @end + +@implementation XCTestCase (LocalIDIntegrity) + +- (OCDatabaseItemFilter)databaseSanityCheckFilter +{ + // This filter block checks if fileIDs and localIDs are used consistently in database updates, i.e. if different localIDs are used for the same fileID - + // or if the items that replace placeholder items use a different localID than the placeholder item. Since all changes end up in the database, this provides + // a high-level sanity check that will assert if violated. + NSMutableDictionary *fileIDByLocalID = [NSMutableDictionary new]; + NSMutableDictionary *localIDByFileID = [NSMutableDictionary new]; + NSMutableDictionary *localIDByPlaceholderPath = [NSMutableDictionary new]; + + return (^(NSArray *items) { + @synchronized (fileIDByLocalID) { + for (OCItem *item in items) + { + OCFileID fileID = item.fileID; + OCLocalID localID = item.localID; + + if (item.isPlaceholder) + { + if ((localID != nil) && (item.path != nil)) + { + OCLocalID savedLocalID = localIDByPlaceholderPath[item.path]; + + if (savedLocalID == nil) + { + localIDByPlaceholderPath[item.path] = localID; + } + else + { + XCTAssert([savedLocalID isEqualToString:localID], @"***> localID not matching expected localID: path=%@, localID=%@, expectedLocalID=%@", item.path, localID, savedLocalID); + } + } + } + else + { + if ((fileID != nil) && (localID != nil)) + { + OCLocalID savedLocalID = localIDByFileID[fileID]; + OCFileID savedFileID = fileIDByLocalID[localID]; + + if (savedFileID!=nil) + { + XCTAssert([savedFileID isEqualToString:fileID], @"***> fileID not matching expected localID: fileID=%@, localID=%@, expectedFileID=%@", fileID, localID, savedFileID); + } + else + { + fileIDByLocalID[localID] = fileID; + } + + if (savedLocalID != nil) + { + XCTAssert([savedLocalID isEqualToString:localID], @"***> localID not matching expected localID: fileID=%@, localID=%@, expectedLocalID=%@, item=%@ via %@", fileID, localID, savedLocalID, item, item.creationHistory); + } + else + { + if (item.path != nil) + { + if ((savedLocalID = localIDByPlaceholderPath[item.path]) != nil) + { + XCTAssert([savedLocalID isEqualToString:localID], @"***> localID not matching expected localID: path=%@, localID=%@, expectedLocalID=%@", item.path, localID, savedLocalID); + } + } + + localIDByFileID[fileID] = localID; + } + } + } + } + } + + return (items); + }); +} + +@end