Skip to content

Commit

Permalink
Import Data from Safari iOS
Browse files Browse the repository at this point in the history
Add Password Importer
Add History Importer
  • Loading branch information
Brandon-T committed Dec 6, 2024
1 parent 9d5f1ff commit ea6722c
Show file tree
Hide file tree
Showing 39 changed files with 3,082 additions and 268 deletions.
4 changes: 4 additions & 0 deletions ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import("//brave/ios/browser/api/session_restore/headers.gni")
import("//brave/ios/browser/api/skus/headers.gni")
import("//brave/ios/browser/api/storekit_receipt/headers.gni")
import("//brave/ios/browser/api/unicode/headers.gni")
import("//brave/ios/browser/api/unzip/headers.gni")
import("//brave/ios/browser/api/url/headers.gni")
import("//brave/ios/browser/api/url_sanitizer/headers.gni")
import("//brave/ios/browser/api/web/ui/headers.gni")
Expand Down Expand Up @@ -81,6 +82,8 @@ brave_core_public_headers = [
"//brave/ios/browser/api/bookmarks/brave_bookmarks_observer.h",
"//brave/ios/browser/api/bookmarks/importer/brave_bookmarks_importer.h",
"//brave/ios/browser/api/bookmarks/exporter/brave_bookmarks_exporter.h",
"//brave/ios/browser/api/history/importer/brave_history_importer.h",
"//brave/ios/browser/api/password/importer/brave_password_importer.h",
"//brave/ios/browser/api/history/brave_history_api.h",
"//brave/ios/browser/api/history/brave_history_observer.h",
"//brave/ios/browser/api/net/certificate_utility.h",
Expand Down Expand Up @@ -129,6 +132,7 @@ brave_core_public_headers += developer_options_code_public_headers
brave_core_public_headers += browser_api_storekit_receipt_public_headers
brave_core_public_headers += webcompat_reporter_public_headers
brave_core_public_headers += browser_api_unicode_public_headers
brave_core_public_headers += browser_api_unzip_public_headers

action("brave_core_umbrella_header") {
script = "//build/config/ios/generate_umbrella_header.py"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class BookmarksViewController: SiteTableViewController, ToolbarUrlActionsProtoco
return self.currentFolder == nil
}

private let importExportUtility = BraveCoreImportExportUtility()
private let importExportUtility = BookmarksImportExportUtility()
private var documentInteractionController: UIDocumentInteractionController?

private var searchBookmarksTimer: Timer?
Expand Down Expand Up @@ -269,7 +269,7 @@ class BookmarksViewController: SiteTableViewController, ToolbarUrlActionsProtoco
alert.popoverPresentationController?.barButtonItem = sender
let importAction = UIAlertAction(title: Strings.bookmarksImportAction, style: .default) {
[weak self] _ in
let vc = UIDocumentPickerViewController(forOpeningContentTypes: [.html])
let vc = UIDocumentPickerViewController(forOpeningContentTypes: [.html, .zip])
vc.delegate = self
self?.present(vc, animated: true)
}
Expand Down Expand Up @@ -967,9 +967,8 @@ extension BookmarksViewController {
func importBookmarks(from url: URL) {
isLoading = true

self.importExportUtility.importBookmarks(from: url) { [weak self] success in
guard let self = self else { return }

Task { @MainActor in
let success = await self.importExportUtility.importBookmarks(from: url)
self.isLoading = false

let alert = UIAlertController(
Expand All @@ -987,8 +986,8 @@ extension BookmarksViewController {
func exportBookmarks(to url: URL) {
isLoading = true

self.importExportUtility.exportBookmarks(to: url) { [weak self] success in
guard let self = self else { return }
Task { @MainActor in
let success = await self.importExportUtility.exportBookmarks(to: url)

self.isLoading = false

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// Copyright 2020 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import BraveCore
import BraveShared
import Data
import Foundation
import Shared
import os.log

class BookmarksImportExportUtility {

// Import an array of bookmarks into BraveCore
@MainActor
func importBookmarks(
from array: [BraveImportedBookmark]
) async -> Bool {
precondition(
state == .none,
"Bookmarks Import - Error Importing while an Import/Export operation is in progress"
)

state = .importing
return await withCheckedContinuation { continuation in
self.importer.import(from: array, topLevelFolderName: Strings.Sync.importFolderName) {
state in

self.state = .none
switch state {
case .started:
Logger.module.debug("Bookmarks Import - Import Started")
case .cancelled:
Logger.module.debug("Bookmarks Import - Import Cancelled")
continuation.resume(returning: false)
case .autoCompleted, .completed:
Logger.module.debug("Bookmarks Import - Import Completed")
continuation.resume(returning: true)
@unknown default:
fatalError()
}
}
}
}

// Import bookmarks from a file into BraveCore
@MainActor
func importBookmarks(from path: URL) async -> Bool {
precondition(
state == .none,
"Bookmarks Import - Error Importing while an Import/Export operation is in progress"
)

let doImport = { (path: URL, nativePath: String) async -> Bool in
await withCheckedContinuation { continuation in
self.importer.import(
fromFile: nativePath,
topLevelFolderName: Strings.Sync.importFolderName,
automaticImport: true
) { [weak self] state, bookmarks in
guard let self else {
continuation.resume(returning: false)
return
}

self.state = .none
switch state {
case .started:
Logger.module.debug("Bookmarks Import - Import Started")
case .cancelled:
Logger.module.debug("Bookmarks Import - Import Cancelled")
continuation.resume(returning: false)
case .autoCompleted, .completed:
Logger.module.debug("Bookmarks Import - Import Completed")
continuation.resume(returning: true)
@unknown default:
fatalError()
}
}
}
}

guard let nativePath = BookmarksImportExportUtility.nativeURLPathFromURL(path) else {
Logger.module.error("Bookmarks Import - Invalid FileSystem Path")
return false
}

// While accessing document URL from UIDocumentPickerViewController to access the file
// startAccessingSecurityScopedResource should be called for that URL
// Reference: https://stackoverflow.com/a/73912499/2239348
guard path.startAccessingSecurityScopedResource() else {
return false
}

state = .importing
defer { state = .none }

if path.pathExtension.lowercased() == "zip" {
guard
let importsPath = try? await uniqueFileName(
"SafariImports",
folder: AsyncFileManager.default.temporaryDirectory
)
else {
return false
}

guard let nativeImportsPath = BookmarksImportExportUtility.nativeURLPathFromURL(importsPath)
else {
return false
}

do {
try await AsyncFileManager.default.createDirectory(
at: importsPath,
withIntermediateDirectories: true
)
} catch {
return false
}

defer {
Task {
try await AsyncFileManager.default.removeItem(at: importsPath)
}
}

if await Unzip.unzip(nativePath, toDirectory: nativeImportsPath) {
let bookmarksFileURL = importsPath.appending(path: "Bookmarks").appendingPathExtension(
"html"
)
guard
let nativeBookmarksPath = BookmarksImportExportUtility.nativeURLPathFromURL(
bookmarksFileURL
)
else {
Logger.module.error("Bookmarks Import - Invalid FileSystem Path")
return false
}

// Each call to startAccessingSecurityScopedResource must be balanced with a call to stopAccessingSecurityScopedResource
// (Note: this is not reference counted)
defer { path.stopAccessingSecurityScopedResource() }
return await doImport(bookmarksFileURL, nativeBookmarksPath)
}

return false
}

// Each call to startAccessingSecurityScopedResource must be balanced with a call to stopAccessingSecurityScopedResource
// (Note: this is not reference counted)
defer { path.stopAccessingSecurityScopedResource() }
return await doImport(path, nativePath)
}

// Export bookmarks from BraveCore to a file
@MainActor
func exportBookmarks(to path: URL) async -> Bool {
precondition(
state == .none,
"Bookmarks Import - Error Exporting while an Import/Export operation is in progress"
)

guard let nativePath = BookmarksImportExportUtility.nativeURLPathFromURL(path) else {
Logger.module.error("Bookmarks Export - Invalid FileSystem Path")
return false
}

self.state = .exporting
return await withCheckedContinuation { continuation in
self.exporter.export(toFile: nativePath) { [weak self] state in
guard let self = self else {
continuation.resume(returning: false)
return
}

self.state = .none
switch state {
case .started:
Logger.module.debug("Bookmarks Export - Export Started")
case .errorCreatingFile, .errorWritingHeader, .errorWritingNodes:
Logger.module.debug("Bookmarks Export - Export Error")
continuation.resume(returning: false)
case .cancelled:
Logger.module.debug("Bookmarks Export - Export Cancelled")
continuation.resume(returning: false)
case .completed:
Logger.module.debug("Bookmarks Export - Export Completed")
continuation.resume(returning: true)
@unknown default:
fatalError()
}
}
}
}

// MARK: - Private
private var state: State = .none
private let importer = BraveBookmarksImporter()
private let exporter = BraveBookmarksExporter()

private enum State {
case importing
case exporting
case none
}
}

// MARK: - Parsing
extension BookmarksImportExportUtility {
static func nativeURLPathFromURL(_ url: URL) -> String? {
return url.withUnsafeFileSystemRepresentation { bytes -> String? in
guard let bytes = bytes else { return nil }
return String(cString: bytes)
}
}

func uniqueFileName(_ filename: String, folder: URL) async throws -> URL {
let basePath = folder.appending(path: filename)
let fileExtension = basePath.pathExtension
let filenameWithoutExtension =
!fileExtension.isEmpty ? String(filename.dropLast(fileExtension.count + 1)) : filename

var proposedPath = basePath
var count = 0

while await AsyncFileManager.default.fileExists(atPath: proposedPath.path) {
count += 1

let proposedFilenameWithoutExtension = "\(filenameWithoutExtension) (\(count))"
proposedPath = folder.appending(path: proposedFilenameWithoutExtension)
.appending(path: fileExtension)
}

return proposedPath
}
}
Loading

0 comments on commit ea6722c

Please sign in to comment.