drive_sync_flutter 1.1.0
drive_sync_flutter: ^1.1.0 copied to clipboard
Bidirectional file sync between device storage and Google Drive. SHA256-based change detection, pluggable conflict resolution, nested folder support.
drive_sync_flutter #
Bidirectional file sync between device storage and Google Drive, with conflict resolution.
Features #
- Sandboxed Drive access — all operations scoped to
.app/{appName}/, preventing path traversal - Push, pull, or bidirectional sync of any local directory to a Google Drive folder
- SHA256-based change detection — only transfers files that actually changed
- Conflict resolution — newerWins, localWins, remoteWins, or askUser (return conflicts to your UI)
- Query injection protection — all Drive API queries are escaped
- Pluggable adapter interface — Google Drive included; implement
DriveAdapterfor iCloud, S3, etc. - No database — state tracked via a single JSON manifest file
- Platform-agnostic — sync engine uses callbacks for file I/O, no direct
dart:iodependency
Installation #
dependencies:
drive_sync_flutter:
git:
url: https://github.com/ajitgunturi/drive_sync_flutter.git
Quick Start #
import 'package:google_sign_in/google_sign_in.dart';
import 'package:drive_sync_flutter/drive_sync_flutter.dart';
// 1. Authenticate with Google
final googleSignIn = GoogleSignIn(scopes: ['https://www.googleapis.com/auth/drive']);
final account = await googleSignIn.signIn();
final authClient = GoogleAuthClient(await account!.authHeaders);
// 2. Create a sandboxed adapter — scoped to .app/my_app/backups on Drive
final adapter = GoogleDriveAdapter.sandboxed(
httpClient: authClient,
appName: 'my_app', // lowercase snake_case, required
subPath: 'backups', // optional subfolder
);
// 3. Create a sync client
final client = DriveSyncClient(
adapter: adapter,
defaultStrategy: ConflictStrategy.newerWins,
);
// 4. Sync!
final result = await client.sync(localPath: '/path/to/local/data');
print('${result.filesUploaded} uploaded, ${result.filesDownloaded} downloaded');
Security #
Sandboxed Drive Access #
GoogleDriveAdapter.sandboxed() enforces that all file operations are scoped under .app/{appName}/ on the user's Google Drive. This prevents:
- Path traversal —
..segments inappNameorsubPathare rejected at construction time - Arbitrary folder creation — the
.app/prefix is mandatory and cannot be bypassed - Query injection — all values interpolated into Drive API queries are escaped
The appName parameter must be lowercase snake_case (^[a-z][a-z0-9_]*$). This is validated eagerly — an invalid name throws ArgumentError before any adapter instance is created.
// Valid
GoogleDriveAdapter.sandboxed(httpClient: client, appName: 'my_app');
GoogleDriveAdapter.sandboxed(httpClient: client, appName: 'my_app', subPath: 'Backups');
// Throws ArgumentError
GoogleDriveAdapter.sandboxed(httpClient: client, appName: 'MyApp'); // uppercase
GoogleDriveAdapter.sandboxed(httpClient: client, appName: '../hack'); // traversal
GoogleDriveAdapter.sandboxed(httpClient: client, appName: ''); // empty
Drive Folder Structure #
User's Google Drive
└── .app/
└── my_app/
├── backups/ ← subPath: 'backups'
│ ├── file1.json
│ └── file2.json
└── plans/ ← subPath: 'plans'
├── week1.json
└── week2.json
Deprecated Constructors #
The unsandboxed constructors (GoogleDriveAdapter() and GoogleDriveAdapter.withPath()) are deprecated. They still work but emit deprecation warnings. Use .sandboxed() for all new code.
API #
DriveSyncClient #
The high-level API for syncing a local directory with a remote folder.
final client = DriveSyncClient(adapter: adapter);
// Bidirectional sync (default) — new files go both ways, conflicts resolved by strategy
final result = await client.sync(localPath: '/data');
// Push only — local overwrites remote
await client.push(localPath: '/data');
// Pull only — remote overwrites local
await client.pull(localPath: '/data');
// Check what would change without syncing
final status = await client.status(localPath: '/data');
print('Pending: ${status.pendingChanges?.totalChanges ?? 0}');
GoogleDriveAdapter #
Handles Google Drive file operations. The .sandboxed() constructor is the recommended way to create an adapter.
// Sandboxed (recommended) — scoped to .app/my_app/data on Drive
final adapter = GoogleDriveAdapter.sandboxed(
httpClient: authClient,
appName: 'my_app',
subPath: 'data',
);
// Multiple folders for the same app
final plans = GoogleDriveAdapter.sandboxed(
httpClient: authClient,
appName: 'my_app',
subPath: 'plans', // .app/my_app/plans
);
final backups = GoogleDriveAdapter.sandboxed(
httpClient: authClient,
appName: 'my_app',
subPath: 'backups', // .app/my_app/backups
);
GoogleAuthClient #
Convenience wrapper that injects Google auth headers into HTTP requests. Bridges google_sign_in with googleapis.
final account = await GoogleSignIn(scopes: ['drive']).signIn();
final authClient = GoogleAuthClient(await account!.authHeaders);
// Pass authClient to GoogleDriveAdapter.sandboxed()
SandboxValidator #
Utility class for path validation. Used internally by GoogleDriveAdapter.sandboxed() but also available for custom validation.
SandboxValidator.validateAppName('my_app'); // passes
SandboxValidator.validateAppName('MyApp'); // throws ArgumentError
SandboxValidator.validateSubPath('Plans'); // passes
SandboxValidator.validateSubPath('../etc'); // throws ArgumentError
SandboxValidator.buildSandboxPath('my_app', 'data'); // returns '.app/my_app/data'
SandboxValidator.escapeDriveQuery("it's"); // returns "it\\'s"
Conflict Resolution #
When both local and remote have modified the same file, the library picks one version — it does NOT merge content. There is no three-way merge, no content-aware diffing. It compares SHA256 checksums (to detect changes) and lastModified timestamps (to pick a winner). This works equally well for JSON, binary, or encrypted files since it never reads file contents.
| Strategy | Behavior |
|---|---|
newerWins |
Most recent lastModified wins. Ties go to local. |
localWins |
Always keep the local version (remote is overwritten). |
remoteWins |
Always keep the remote version (local is overwritten). |
askUser |
Skips the file and returns it in result.unresolvedConflicts for your UI to handle. |
Important: The losing version is overwritten. If you need to preserve both versions, use askUser and implement your own merge or backup logic.
final client = DriveSyncClient(
adapter: adapter,
defaultStrategy: ConflictStrategy.remoteWins, // plans folder: remote is truth
);
Custom Adapters #
Implement DriveAdapter to sync with any cloud provider:
class S3Adapter implements DriveAdapter {
@override
Future<void> ensureFolder() async { /* create bucket/prefix */ }
@override
Future<Map<String, RemoteFileInfo>> listFiles() async { /* list objects */ }
@override
Future<void> uploadFile(String path, List<int> content) async { /* PUT object */ }
@override
Future<List<int>> downloadFile(String path) async { /* GET object */ }
@override
Future<void> deleteFile(String path) async { /* DELETE object */ }
}
Architecture #
DriveSyncClient <- High-level API (sync/push/pull/status)
|
+-- SyncEngine <- Orchestrates diff -> resolve -> transfer
|
+-- ManifestDiffer <- Compares file states (added/modified/deleted/unchanged)
+-- ConflictResolver <- Applies conflict strategy
+-- DriveAdapter <- File I/O interface
|
+-- GoogleDriveAdapter <- Google Drive v3 implementation
|
+-- SandboxValidator <- Path validation + query escaping
Manifest: A JSON file (_sync_manifest.json) stored alongside your local data tracks {path, sha256, lastModified} for each synced file. Only files that changed since the last sync are transferred.
Scope and Boundaries #
What this library does #
- Syncs files (any format: JSON, YAML, images, binary, encrypted blobs) between a local directory and a Google Drive folder
- Detects changes via SHA256 checksums — only transfers files that actually differ
- Resolves conflicts when the same file is modified locally and remotely
- Creates nested folder hierarchies on Drive automatically
- Tracks sync state via a local manifest file (
_sync_manifest.json) - Sandboxes all operations under
.app/{appName}/to prevent path traversal
What this library does NOT do #
- No encryption. Files are transferred as-is. If you need encryption, encrypt before syncing and decrypt after pulling.
- No content merging. Conflict resolution picks one version (local or remote) — it never merges file contents.
- No authentication. You must provide an authenticated
http.Client(e.g., viagoogle_sign_in). - No background sync. Sync is triggered explicitly by your code.
- No partial/resumable uploads. Files are uploaded/downloaded in full. Not suitable for files larger than ~50MB.
- No file locking or concurrency control. Designed for single-device use.
Developer responsibility #
| Concern | Who handles it |
|---|---|
| OAuth flow (sign-in, token refresh) | You — use google_sign_in or equivalent |
| Providing an authenticated HTTP client | You — wrap with GoogleAuthClient or your own |
| Encryption of sensitive data | You — encrypt before sync, decrypt after pull |
| File format and schema validation | You — library treats files as opaque bytes |
| Retry logic on network failure | You — library returns errors in SyncResult.errors |
| Background/periodic sync scheduling | You — call sync() when appropriate |
| Google Cloud project setup (OAuth consent, client IDs) | You — required for google_sign_in to work |
Library responsibility #
| Concern | Who handles it |
|---|---|
| Change detection (SHA256) | Library |
| Manifest tracking | Library |
| Conflict resolution | Library (configurable strategy) |
| Google Drive CRUD (list, upload, download, delete) | Library (GoogleDriveAdapter) |
| Folder creation on Drive | Library (nested paths supported) |
| Path sandboxing and validation | Library (SandboxValidator) |
| Query injection prevention | Library (escaped query values) |
| Error reporting per file | Library (via SyncResult.errors) |
Permissions #
Your app needs:
- Google Drive scope:
https://www.googleapis.com/auth/drive(full) orhttps://www.googleapis.com/auth/drive.file(app-created files only) - Internet access
- Device storage (read/write local files)
Testing #
dart test
72 tests covering manifest diffing, conflict resolution, sync engine flows, sandbox validation, path enforcement, query injection prevention, and the full DriveSyncClient lifecycle.
License #
MIT