Skip to content

Commit

Permalink
Sync Engine with support for syncing bookmarks (#1724)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1204606874917765/f

Description:
Add support for syncing bookmarks
  • Loading branch information
ayoy authored May 26, 2023
1 parent 8e651e9 commit 286fd55
Show file tree
Hide file tree
Showing 32 changed files with 584 additions and 129 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ file_header:
# General Config
excluded:
- Carthage
- DerivedData
- DuckDuckGo/UserText.swift
- fastlane/SnapshotHelper.swift
- vendor
Expand Down
10 changes: 7 additions & 3 deletions Core/BookmarksCachingSearch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,15 @@ public class CoreDataBookmarksSearchStore: BookmarksSearchStore {
let context = bookmarksStore.makeContext(concurrencyType: .privateQueueConcurrencyType)

let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "BookmarkEntity")
fetchRequest.predicate = NSPredicate(format: "%K = false", #keyPath(BookmarkEntity.isFolder))
fetchRequest.predicate = NSPredicate(
format: "%K = false AND %K == NO",
#keyPath(BookmarkEntity.isFolder),
#keyPath(BookmarkEntity.isPendingDeletion)
)
fetchRequest.resultType = .dictionaryResultType
fetchRequest.propertiesToFetch = [#keyPath(BookmarkEntity.title),
#keyPath(BookmarkEntity.url),
#keyPath(BookmarkEntity.isFavorite),
#keyPath(BookmarkEntity.favoriteFolder),
#keyPath(BookmarkEntity.objectID)]

context.perform {
Expand Down Expand Up @@ -127,7 +131,7 @@ public class BookmarksCachingSearch: BookmarksStringSearch {
self.init(objectID: objectID,
title: title,
url: url,
isFavorite: (bookmark[#keyPath(BookmarkEntity.isFavorite)] as? NSNumber)?.boolValue ?? false)
isFavorite: bookmark[#keyPath(BookmarkEntity.favoriteFolder)] != nil)
}

public func togglingFavorite() -> BookmarksStringSearchResult {
Expand Down
40 changes: 40 additions & 0 deletions Core/BookmarksCleanupErrorHandling.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// BookmarksCleanupErrorHandling.swift
// DuckDuckGo
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import Bookmarks
import Common
import Persistence

public class BookmarksCleanupErrorHandling: EventMapping<BookmarksCleanupError> {

public init() {
super.init { event, _, _, _ in
let domainEvent = Pixel.Event.bookmarksCleanupFailed
let processedErrors = CoreDataErrorsParser.parse(error: event.coreDataError as NSError)
let params = processedErrors.errorPixelParameters

Pixel.fire(pixel: domainEvent, error: event.coreDataError, withAdditionalParameters: params)
}
}

override init(mapping: @escaping EventMapping<BookmarksCleanupError>.Mapping) {
fatalError("Use init()")
}
}
5 changes: 3 additions & 2 deletions Core/BookmarksExporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ public struct BookmarksExporter {
guard let rootFolder = BookmarkUtils.fetchRootFolder(context) else {
throw BookmarksExporterError.brokenDatabaseStructure
}

let topLevelBookmarksAndFavorites = rootFolder.childrenArray

let orphanedBookmarks = BookmarkUtils.fetchOrphanedEntities(context)
let topLevelBookmarksAndFavorites = rootFolder.childrenArray + orphanedBookmarks
content.append(contentsOf: export(topLevelBookmarksAndFavorites, level: 2))
content.append(Template.footer)
return content.joined()
Expand Down
5 changes: 4 additions & 1 deletion Core/Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public extension OSLog {
case lifecycleLog = "DDG Lifecycle"
case autoconsentLog = "DDG Autoconsent"
case configurationLog = "DDG Configuration"
case syncLog = "DDG Sync"
}

@OSLogWrapper(.generalLog) static var generalLog
Expand All @@ -38,6 +39,7 @@ public extension OSLog {
@OSLogWrapper(.lifecycleLog) static var lifecycleLog
@OSLogWrapper(.autoconsentLog) static var autoconsentLog
@OSLogWrapper(.configurationLog) static var configurationLog
@OSLogWrapper(.syncLog) static var syncLog

// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// To activate Logging Categories add categories here:
Expand All @@ -47,7 +49,8 @@ public extension OSLog {
.contentBlockingLog,
.adAttributionLog,
.lifecycleLog,
.configurationLog
.configurationLog,
.syncLog
]
#endif

Expand Down
12 changes: 11 additions & 1 deletion Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -401,13 +401,18 @@ extension Pixel {

case bookmarksCouldNotLoadDatabase
case bookmarksCouldNotPrepareDatabase
case bookmarksCleanupFailed
case bookmarksMigrationAlreadyPerformed
case bookmarksMigrationFailed
case bookmarksMigrationCouldNotPrepareDatabase
case bookmarksMigrationCouldNotPrepareDatabaseOnFailedMigration
case bookmarksMigrationCouldNotValidateDatabase
case bookmarksMigrationCouldNotRemoveOldStore


case syncMetadataCouldNotLoadDatabase
case syncBookmarksProviderInitializationFailed
case syncBookmarksFailed

case invalidPayload(Configuration)
}

Expand Down Expand Up @@ -797,6 +802,7 @@ extension Pixel.Event {

case .bookmarksCouldNotLoadDatabase: return "m_d_bookmarks_could_not_load_database"
case .bookmarksCouldNotPrepareDatabase: return "m_d_bookmarks_could_not_prepare_database"
case .bookmarksCleanupFailed: return "m_d_bookmarks_cleanup_failed"
case .bookmarksMigrationAlreadyPerformed: return "m_d_bookmarks_migration_already_performed"
case .bookmarksMigrationFailed: return "m_d_bookmarks_migration_failed"
case .bookmarksMigrationCouldNotPrepareDatabase: return "m_d_bookmarks_migration_could_not_prepare_database"
Expand All @@ -805,6 +811,10 @@ extension Pixel.Event {
case .bookmarksMigrationCouldNotValidateDatabase: return "m_d_bookmarks_migration_could_not_validate_database"
case .bookmarksMigrationCouldNotRemoveOldStore: return "m_d_bookmarks_migration_could_not_remove_old_store"

case .syncMetadataCouldNotLoadDatabase: return "m_d_sync_metadata_could_not_load_database"
case .syncBookmarksProviderInitializationFailed: return "m_d_sync_bookmarks_provider_initialization_failed"
case .syncBookmarksFailed: return "m_d_sync_bookmarks_failed"

case .invalidPayload(let configuration): return "m_d_\(configuration.rawValue)_invalid_payload".lowercased()
}

Expand Down
75 changes: 75 additions & 0 deletions Core/SyncBookmarksAdapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// SyncBookmarksAdapter.swift
// DuckDuckGo
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine
import Common
import DDGSync
import Foundation
import Persistence
import SyncDataProviders

public final class SyncBookmarksAdapter {

public private(set) var provider: BookmarksProvider?

public let syncDidCompletePublisher: AnyPublisher<Void, Never>

public init() {
syncDidCompletePublisher = syncDidCompleteSubject.eraseToAnyPublisher()
}

public func setUpProviderIfNeeded(database: CoreDataDatabase, metadataStore: SyncMetadataStore) {
guard provider == nil else {
return
}
do {
let provider = try BookmarksProvider(
database: database,
metadataStore: metadataStore,
reloadBookmarksAfterSync: { [syncDidCompleteSubject] in
syncDidCompleteSubject.send()
}
)

syncErrorCancellable = provider.syncErrorPublisher
.sink { error in
switch error {
case let syncError as SyncError:
Pixel.fire(pixel: .syncBookmarksFailed, error: syncError)
default:
let nsError = error as NSError
if nsError.domain != NSURLErrorDomain {
let processedErrors = CoreDataErrorsParser.parse(error: error as NSError)
let params = processedErrors.errorPixelParameters
Pixel.fire(pixel: .syncBookmarksFailed, error: error, withAdditionalParameters: params)
}
}
os_log(.error, log: OSLog.syncLog, "Bookmarks Sync error: %{public}s", String(reflecting: error))
}
self.provider = provider
} catch let error as NSError {
let processedErrors = CoreDataErrorsParser.parse(error: error)
let params = processedErrors.errorPixelParameters
Pixel.fire(pixel: .syncBookmarksProviderInitializationFailed, error: error, withAdditionalParameters: params)
}
}

private var syncDidCompleteSubject = PassthroughSubject<Void, Never>()
private var syncErrorCancellable: AnyCancellable?
}
70 changes: 70 additions & 0 deletions Core/SyncDataProviders.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// SyncDataProviders.swift
// DuckDuckGo
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Common
import DDGSync
import Persistence
import SyncDataProviders

public class SyncDataProviders: DataProvidersSource {
public let bookmarksAdapter: SyncBookmarksAdapter

public func makeDataProviders() -> [DataProviding] {
initializeMetadataDatabaseIfNeeded()
guard let syncMetadata else {
assertionFailure("Sync Metadata not initialized")
return []
}

bookmarksAdapter.setUpProviderIfNeeded(database: bookmarksDatabase, metadataStore: syncMetadata)
return [bookmarksAdapter.provider].compactMap { $0 }
}

public init(bookmarksDatabase: CoreDataDatabase) {
self.bookmarksDatabase = bookmarksDatabase
bookmarksAdapter = SyncBookmarksAdapter()
}

private func initializeMetadataDatabaseIfNeeded() {
guard !isSyncMetadaDatabaseLoaded else {
return
}

syncMetadataDatabase.loadStore { context, error in
guard context != nil else {
if let error = error {
Pixel.fire(pixel: .syncMetadataCouldNotLoadDatabase, error: error)
} else {
Pixel.fire(pixel: .syncMetadataCouldNotLoadDatabase)
}

Thread.sleep(forTimeInterval: 1)
fatalError("Could not create Sync Metadata database stack: \(error?.localizedDescription ?? "err")")
}
}
syncMetadata = LocalSyncMetadataStore(database: syncMetadataDatabase)
isSyncMetadaDatabaseLoaded = true
}

private var isSyncMetadaDatabaseLoaded: Bool = false
private var syncMetadata: SyncMetadataStore?

private let syncMetadataDatabase: CoreDataDatabase = SyncMetadataDatabase.make()
private let bookmarksDatabase: CoreDataDatabase
}
54 changes: 54 additions & 0 deletions Core/SyncMetadataDatabase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// SyncMetadataDatabase.swift
// DuckDuckGo
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import CoreData
import DDGSync
import Persistence
import Common

public final class SyncMetadataDatabase {

private init() { }

public static var defaultDBLocation: URL = {
guard let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
os_log("SyncMetadataDatabase.make - OUT, failed to get location")
fatalError("Failed to get location")
}
return url
}()

public static func make(location: URL = defaultDBLocation, readOnly: Bool = false) -> CoreDataDatabase {
os_log("SyncMetadataDatabase.make - IN - %s", location.absoluteString)
let bundle = DDGSync.bundle
guard let model = CoreDataDatabase.loadModel(from: bundle, named: "SyncMetadata") else {
os_log("SyncMetadataDatabase.make - OUT, failed to loadModel")
fatalError("Failed to load model")
}

let db = CoreDataDatabase(name: "SyncMetadata",
containerLocation: location,
model: model,
readOnly: readOnly)
os_log("SyncMetadataDatabase.make - OUT")
return db
}

}
Loading

0 comments on commit 286fd55

Please sign in to comment.