blob: f275fcd7d2286fb1f8cd42b6be0944d961181721 [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import Foundation
import SyncbaseCore
/// Syncbase is a storage system for developers that makes it easy to synchronize app data between
/// devices. It works even when devices are not connected to the Internet.
public enum Syncbase {
// Constants
static let TAG = "syncbase",
DIR_NAME = "syncbase",
DB_NAME = "db",
USERDATA_SYNCGROUP_NAME = "userdata__"
// Initialization state
static var didInit = false
static var didPostLogin = false
static var didStartShutdown = false
// Main database.
static var db: Database?
// The userData collection, created post-login.
static var userDataCollection: Collection?
// Options for opening a database.
static var adminUserId = "alexfandrianto@google.com"
static var defaultBlessingStringPrefix = "dev.v.io:o:608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com:"
static var disableSyncgroupPublishing = false
static var disableUserdataSyncgroup = false
static var mountPoints = ["/ns.dev.v.io:8101/tmp/todos/users/"]
static var rootDir = Syncbase.defaultRootDir
/// Queue used to dispatch all asynchronous callbacks. Defaults to main.
public static var queue: dispatch_queue_t {
// Map directly to SyncbaseCore.
get {
return SyncbaseCore.Syncbase.queue
}
set(queue) {
SyncbaseCore.Syncbase.queue = queue
}
}
static public var defaultRootDir: String {
return NSFileManager.defaultManager()
.URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]
.URLByAppendingPathComponent("Syncbase")
.absoluteString
}
static var publishSyncbaseName: String? {
if Syncbase.disableSyncgroupPublishing {
return nil
}
return mountPoints[0] + "cloud"
}
static var cloudBlessing: String {
return "dev.v.io:u:" + Syncbase.adminUserId
}
/// Starts Syncbase if needed; creates default database if needed; performs create-or-join for
/// "userdata" syncgroup if needed.
///
/// The "userdata" collection is a per-user collection (and associated syncgroup) for data that
/// should automatically get synced across a given user's devices. It has the following schema:
/// - `/syncgroups/{encodedSyncgroupId}` -> `nil`
/// - `/ignoredInvites/{encodedSyncgroupId}` -> `nil`
///
/// - parameter adminUserId: The email address for the administrator user.
/// - parameter rootDir: Where data should be persisted.
/// - parameter mountPoints: // TODO(zinman): Get appropriate documentation on mountPoints
/// - parameter defaultBlessingStringPrefix: // TODO(zinman): Figure out what this should default to.
/// - parameter disableSyncgroupPublishing: **FOR ADVANCED USERS**. If true, syncgroups will not be published to the cloud peer.
/// - parameter disableUserdataSyncgroup: **FOR ADVANCED USERS**. If true, the user's data will not be synced across their devices.
/// - parameter callback: Called on `Syncbase.queue` with either `Database` if successful, or an error if unsuccessful.
public static func configure(
adminUserId adminUserId: String,
// Default to Application Support/Syncbase.
rootDir: String = NSFileManager.defaultManager()
.URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]
.URLByAppendingPathComponent("Syncbase")
.path!,
mountPoints: [String] = ["/ns.dev.v.io:8101/tmp/todos/users/"],
defaultBlessingStringPrefix: String = "dev.v.io:o:608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com:",
disableSyncgroupPublishing: Bool = false,
disableUserdataSyncgroup: Bool = false,
queue: dispatch_queue_t = dispatch_get_main_queue()) throws {
if Syncbase.didInit {
throw SyncbaseError.AlreadyConfigured
}
Syncbase.adminUserId = adminUserId
Syncbase.rootDir = rootDir
Syncbase.mountPoints = mountPoints
Syncbase.defaultBlessingStringPrefix = defaultBlessingStringPrefix
Syncbase.disableSyncgroupPublishing = disableSyncgroupPublishing
Syncbase.disableUserdataSyncgroup = disableUserdataSyncgroup
Syncbase.didPostLogin = false
Syncbase.didStartShutdown = false
// We don't need to set Syncbase.queue as it is a proxy for SyncbaseCore's queue, which is
// set in the configure below.
try SyncbaseError.wrap {
try SyncbaseCore.Syncbase.configure(rootDir: Syncbase.rootDir, queue: queue)
}
// We use SyncbaseCore's isLoggedIn because this frameworks would fail as didInit hasn't
// been set to true yet.
if (SyncbaseCore.Syncbase.isLoggedIn) {
do {
try Syncbase.postLoginCreateDefaults()
} catch let e {
// If we get an exception after configuring the low-level API, make sure we shutdown
// Syncbase so that any subsequent call to this configure method doesn't get a
// SyncbaseError.AlreadyConfigured exception from SyncbaseCore.Syncbase.configure.
Syncbase.shutdown()
throw e
}
}
Syncbase.didInit = true
}
/// Shuts down the Syncbase service. You must call configure again before any calls will work.
public static func shutdown() {
Syncbase.didStartShutdown = true
SyncbaseCore.Syncbase.shutdown()
Syncbase.didInit = false
Syncbase.didPostLogin = false
}
private static func postLoginCreateDefaults() throws {
let coreDb = try SyncbaseCore.Syncbase.database(Syncbase.DB_NAME)
let database = Database(coreDatabase: coreDb)
try database.createIfMissing()
userDataCollection = try database.collection(Syncbase.USERDATA_SYNCGROUP_NAME, withoutSyncgroup: true)
if (!Syncbase.disableUserdataSyncgroup) {
let syncgroup = try userDataCollection!.syncgroup()
do {
try syncgroup.join()
} catch {
try syncgroup.createIfMissing([userDataCollection!])
}
// TODO(zinman): Figure out when/how this can throw and if we should handle it better.
try database.addUserDataWatchChangeHandler(
handler: WatchChangeHandler(
onInitialState: onUserDataWatchChange,
onChangeBatch: onUserDataWatchChange,
onError: { err in
if !Syncbase.didStartShutdown {
NSLog("Syncbase - Error watching userdata syncgroups: %@", "\(err)")
}
}))
}
Syncbase.db = database
Syncbase.didPostLogin = true
}
private static func onUserDataWatchChange(changes: [WatchChange]) {
for change in changes {
guard let row = change.row where change.entityType == .Row && change.changeType == .Put else {
continue
}
do {
let syncgroupId = try Identifier.decode(row)
let syncgroup = try Syncbase.database().syncgroup(syncgroupId)
try syncgroup.join()
} catch let e {
NSLog("Syncbase - Error joining syncgroup: %@", "\(e)")
}
}
}
/// Returns the shared database handle. Must have already called `configure` and be logged in,
/// otherwise this will throw a `SyncbaseError.NotConfigured` or `SyncbaseError.NotLoggedIn`
/// error.
public static func database() throws -> Database {
if !Syncbase.didInit {
throw SyncbaseError.NotConfigured
}
guard let db = Syncbase.db where Syncbase.didPostLogin else {
throw SyncbaseError.NotLoggedIn
}
return db
}
public typealias LoginCallback = (err: ErrorType?) -> Void
/// Authorize using an oauth token. Right now only Google OAuth token is supported
/// (you should use the Google Sign In SDK to get this).
///
/// You must login and have valid credentials before you can call `database()` to perform any
/// operation.
///
/// Calls `callback` on `Syncbase.queue` with any error that occured, or nil on success.
public static func login(credentials: GoogleOAuthCredentials, callback: LoginCallback) {
SyncbaseCore.Syncbase.login(
SyncbaseCore.GoogleOAuthCredentials(token: credentials.token),
callback: { err in
guard err == nil else {
if let e = err as? SyncbaseCore.SyncbaseError {
callback(err: SyncbaseError(coreError: e))
} else {
callback(err: err)
}
return
}
// postLoginCreateDefaults can be blocking when performing create-or-join. Run on
// a background queue to prevent blocking from the Go callback.
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
var callbackErr: ErrorType?
do {
try postLoginCreateDefaults()
} catch let e {
callbackErr = e
}
dispatch_async(Syncbase.queue) {
callback(err: callbackErr)
}
}
})
}
public static func isLoggedIn() throws -> Bool {
if !Syncbase.didInit {
throw SyncbaseError.NotConfigured
}
return SyncbaseCore.Syncbase.isLoggedIn
}
}