Package-level declarations

Two layers. CloudStorage is the filesystem-style transport (ICloudStorage, GoogleDriveStorage). SyncEngine owns the complete-or-absent reconciliation over a SyncSnapshot and persists its SyncState through a SyncStateStore. Start with the README quickstart, then SyncEngine.sync.

Two layers. CloudStorage is the filesystem-style transport (ICloudStorage, GoogleDriveStorage). SyncEngine owns the complete-or-absent reconciliation over a SyncSnapshot and persists its SyncState through a SyncStateStore. Start with the README quickstart, then SyncEngine.sync.

Two layers. CloudStorage is the filesystem-style transport (ICloudStorage, GoogleDriveStorage). SyncEngine owns the complete-or-absent reconciliation over a SyncSnapshot and persists its SyncState through a SyncStateStore. Start with the README quickstart, then SyncEngine.sync.

Types

Link copied to clipboard
object BackupKit

Library-wide switches.

Link copied to clipboard

The Ktor client BackupKit uses when the app does not pass its own.

Link copied to clipboard
fun interface BackupKitLogger

Plug your logger in through BackupKit.logger. The default discards everything.

Link copied to clipboard
sealed interface CloudAvailability

Whether the transport can be used right now, without touching the network.

Link copied to clipboard

The only failure vocabulary the library exposes. Render these; never parse messages.

Link copied to clipboard
class CloudKitStorage(checkpointPath: String, cacheDirectory: String, containerIdentifier: String? = null, zoneName: String = "backupkit", recordType: String = "BackupEntry", environment: String = "") : CloudStorage

CloudKit transport: one custom zone in the user's private database, one record per file, the bytes as a CKAsset. Unlike the iCloud Drive container there are no placeholder files: a save completes when Apple's server has the record, and a read is a network fetch with a definite outcome.

Link copied to clipboard

Where the user's own copy lives. One per platform.

Link copied to clipboard
interface CloudStorage

Layer 1: filesystem-style access to the user's own cloud.

Link copied to clipboard
class CloudStorageException(val error: CloudError, message: String, cause: Throwable? = null) : Exception

Every transport failure is mapped to a CloudError the app can show.

Link copied to clipboard
class DriveConsent(context: ERROR CLASS: Symbol not found for Context)

Google's one-time permission dialog for the app-data folder. Call request; when it returns Request.Needed, launch the IntentSender with ActivityResultContracts.StartIntentSenderForResult and hand the result's data to wasGranted. No Compose dependency: the app owns the launcher.

Link copied to clipboard
sealed interface DriveToken

A short-lived OAuth access token for the drive.appdata scope.

Link copied to clipboard

Supplies Drive access tokens. The default, PlayServicesTokenProvider, needs no sign-in UI. Apps that already run Google Sign-In can plug their own token in here instead.

Link copied to clipboard
@RequiresOptIn(message = "RestoreEngine is unit-tested but has not been run on a device by a shipping app yet; its API may change.", level = RequiresOptIn.Level.WARNING)
annotation class ExperimentalRestoreApi

Marks the restore half of BackupKit: unit-tested, not yet run on a device by a shipping app. Opt in knowingly; the API may change in 0.2.0.

Link copied to clipboard

JSON file at path, written atomically. A corrupt or absent file reads as null.

Link copied to clipboard

JSON file at path, written atomically (temp + rename). A corrupt or absent file is an empty state; the next sync self-heals from the remote listing.

Link copied to clipboard
class GoogleDriveStorage(context: ERROR CLASS: Symbol not found for Context, tokenProvider: DriveTokenProvider = PlayServicesTokenProvider(context), httpClient: HttpClient = BackupKitHttp.default()) : CloudStorage

Google Drive app-data transport. Flat folder; paths are used verbatim as Drive file names. Uploads up to 5 MB go multipart in one request; larger ones use Drive's resumable protocol in 8 MiB chunks, streamed from disk for local files. list also removes duplicate files left by interrupted creates, keeping the newest by modified time.

Link copied to clipboard
class ICloudStorage(folder: String = "backupkit", containerIdentifier: String? = null, downloadTimeout: Duration = 30.seconds) : CloudStorage

iCloud Drive transport: plain files under <ubiquity container>/<folder>/, OUTSIDE Documents/, so they stay private to the app and invisible in Files. Writes land locally through NSFileCoordinator and the iCloud daemon uploads on its own. Reads force a download and wait with a bounded poll (a not-yet-downloaded file is never coordinated directly: that blocks).

Link copied to clipboard

Severity of a BackupKitLogger line.

Link copied to clipboard
sealed interface PlacementResult
Link copied to clipboard
class PlayServicesTokenProvider(context: ERROR CLASS: Symbol not found for Context) : DriveTokenProvider

Silent token source for the Drive app-data scope through Play Services' AuthorizationClient. After the user grants once (see DriveConsent), authorize returns a cached one-hour access token with no UI; before that it reports DriveToken.NeedsConsent. No account identity is read or stored.

Link copied to clipboard
data class RemoteFile(val path: String, val size: Long, val remoteId: String?)

One file as the cloud sees it. remoteId is Drive's file id, null on iCloud. size is -1 when unknown (an iCloud placeholder that has not been downloaded yet).

Link copied to clipboard
sealed interface RemoteProbe

Typed, metadata-only inspection of the remote set, for a restore offer.

Link copied to clipboard
class RestoreEngine(engine: SyncEngine, storage: CloudStorage, recordStore: RestoreRecordStore, placement: RestorePlacement = RestorePlacement.KeepDownloaded, clock: () -> Long = { Clock.System.now().toEpochMilliseconds() }, maxAttempts: Int = 3, newRunId: () -> String = { Random.nextLong().toULong().toString(radix = 16) })

Resumable download of a pinned remote set. Required files first (the commit boundary), then optional files best-effort with maxAttempts tries each. Progress is durable per file; resume revalidates the source through SyncEngine.probe before continuing, so a backup that changed under the plan is reported as RestoreError.SourceChanged instead of being mixed in.

Link copied to clipboard
Link copied to clipboard
data class RestoreFile(val path: String, val toLocalPath: String, val remoteId: String? = null, val required: Boolean = true, val group: String? = null)

One file to bring down. required files form the commit boundary: all of them must succeed before any optional file is attempted, and one failure fails the run. Optional files are best-effort with a per-file attempt cap and stay pending across runs.

Link copied to clipboard
@Serializable
data class RestoreFileRecord(val path: String, val toLocalPath: String, val remoteId: String? = null, val required: Boolean = true, val done: Boolean = false, val attempts: Int = 0, val group: String? = null, val downloaded: Boolean = false)
Link copied to clipboard
sealed interface RestoreOutcome
Link copied to clipboard
fun interface RestorePlacement

Where a downloaded group goes. Called once every downloadable file of a group is local (per file when the file has no group), with the records whose RestoreFileRecord.toLocalPath now holds the bytes. The app validates, moves or imports them however it likes and answers. The default keeps files where they landed.

Link copied to clipboard
class RestorePlan(val source: SourceRef, val files: List<RestoreFile>)

What to restore, pinned to the SourceRef the offer was built from (RemoteProbe.Found.source).

Link copied to clipboard
data class RestoreProgress(val filesDone: Int, val filesTotal: Int, val groupsDone: Int, val groupsTotal: Int)

Progress in files and in the app's own unit, its groups.

Link copied to clipboard
@Serializable
data class RestoreRecord(val runId: String, val source: SourceRef, val files: List<RestoreFileRecord>, val startedAtEpochMs: Long, val completedAtEpochMs: Long? = null)

Durable progress of one restore chain. Saved after every file so a killed process resumes exactly where it stopped.

Link copied to clipboard
Link copied to clipboard
data class ShrinkGuard(val maxDeleteFraction: Double = 0.5, val minRemoteEntries: Int = 4)

Refuses a run that would delete more than maxDeleteFraction of the remote set once the remote holds at least minRemoteEntries non-marker files. A device that lost most of its data, or an app bug that projected an empty model, must not turn a healthy backup into a copy of the damage. The run answers UnavailableReason.ShrinkSuspected and writes nothing; the app asks the user and retries with SyncSnapshot(allowShrink = true).

Link copied to clipboard
@Serializable
data class SourceRef(val identityKey: String?, val markerRemoteId: String?, val markerFingerprint: String)

Pins the remote set a restore was offered from, so a later resume can detect a switch to another backup.

Link copied to clipboard
@Serializable
data class SyncedEntry(val size: Long, val hash: String? = null, val remoteId: String? = null)

hash is sha256-hex for hash-compared entries, null for size-compared ones (identity = path + size).

Link copied to clipboard
class SyncEngine(storage: CloudStorage, stateStore: SyncStateStore, policy: SyncPolicy, clock: () -> Long = { Clock.System.now().toEpochMilliseconds() })

Layer 2: ONE reconciliation run per sync. Lists the remote, self-heals the state cache, diffs the snapshot against it, uploads size-compared entries, then hash-compared entries, then the marker, then deletes what is no longer in the snapshot. State is saved after every put, so a killed process resumes exactly where it stopped. See docs/contract.md.

Link copied to clipboard
data class SyncEntry(val path: String, val source: SyncSource, val size: Long, val hash: String? = null)

One file the app wants mirrored. hash (use sha256Hex) makes the entry hash-compared; null makes it size-compared, which suits write-once media. Hash-compared entries upload in snapshot order after the size-compared ones.

Link copied to clipboard
sealed interface SyncOutcome
Link copied to clipboard
data class SyncPolicy(val markerPath: String, val guardEmptyOverExisting: Boolean = true, val shrinkGuard: ShrinkGuard? = ShrinkGuard(), val mimeTypeOf: (path: String) -> String = ::defaultMimeType)
Link copied to clipboard
data class SyncSnapshot(val entries: List<SyncEntry>, val isEmpty: Boolean = entries.none { it.source !is SyncSource.Absent }, val allowShrink: Boolean = false)

The complete desired remote set. Anything the engine knows about that is not here gets deleted.

Link copied to clipboard
sealed interface SyncSource

Where an entry's bytes come from.

Link copied to clipboard
@Serializable
data class SyncState(val provider: String = "", val identityKey: String = "", val entries: Map<String, SyncedEntry> = emptyMap(), val lastSuccessEpochMs: Long? = null, val lastEntryCount: Int = 0, val hold: String = "None")

What the last successful writes left remote. A CACHE: every SyncEngine.sync re-lists the remote and reconciles against it, so a lost or stale state only costs a re-upload, never correctness.

Link copied to clipboard
interface SyncStateStore

Persists SyncState between runs. Implement it yourself or use FileSyncStateStore.

Link copied to clipboard
Link copied to clipboard

Parks SyncEngine.sync while a restore runs or is incomplete. Persisted in SyncState.

Properties

Link copied to clipboard

The Drive scope BackupKit uses: the hidden, app-private folder only.

Functions

Link copied to clipboard

Mime type by file extension, case-insensitive. Unknown extensions are application/octet-stream.

Link copied to clipboard

SHA-256 of bytes as 64 lowercase hex characters. Use it for SyncEntry.hash.