Added Google Drive storage location

This commit is contained in:
Courtney Arnold 2026-08-08 14:59:18 -05:00
parent e630f2a3ec
commit 73a7c93e64
22 changed files with 1690 additions and 205 deletions

View file

@ -0,0 +1,108 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;
import 'drive_oauth_config.dart';
/// Full Drive access is required (not the narrower `drive.file` scope)
/// because users need to browse to and reuse folders that someone else
/// created and shared with them, not just folders/files this app itself
/// created. See the plan doc for the tradeoffs (this requires Google
/// Cloud Console "Testing" mode with explicit test users, to avoid needing
/// a full OAuth verification review).
const driveScopes = <String>['https://www.googleapis.com/auth/drive'];
/// Thrown when a Drive API call needs authorization that isn't currently
/// available without prompting the user, e.g. during a background sync.
class DriveNotAuthorizedException implements Exception {
@override
String toString() => 'Drive access is not currently authorized.';
}
/// Wraps `google_sign_in` for authenticating with Google and producing an
/// authenticated [http.Client] for the Drive API.
class DriveAuthService {
bool _initialized = false;
GoogleSignInAccount? _account;
bool get isSignedIn => _account != null;
String? get currentAccountEmail => _account?.email;
Future<void> _ensureInitialized() async {
if (_initialized) return;
await GoogleSignIn.instance.initialize(
clientId: Platform.isIOS ? DriveOAuthConfig.iosClientId : null,
serverClientId: Platform.isAndroid ? DriveOAuthConfig.androidServerClientId : null,
);
_initialized = true;
}
/// Attempts to restore a previous sign-in without any UI. Returns true if
/// the user is signed in and Drive access is already authorized.
Future<bool> attemptSilentSignIn() async {
await _ensureInitialized();
final account = await GoogleSignIn.instance.attemptLightweightAuthentication();
_account = account;
if (account == null) return false;
final authorization =
await account.authorizationClient.authorizationForScopes(driveScopes);
return authorization != null;
}
/// Interactive sign-in + Drive scope authorization. Must be called from a
/// user-initiated action (e.g. a button press).
Future<String> signIn() async {
await _ensureInitialized();
final account = await GoogleSignIn.instance.authenticate(scopeHint: driveScopes);
_account = account;
await account.authorizationClient.authorizeScopes(driveScopes);
return account.email;
}
Future<void> signOut() async {
await GoogleSignIn.instance.signOut();
_account = null;
}
/// Builds an [http.Client] that attaches a fresh Drive authorization
/// header to every request. Fetches headers per-request (rather than
/// once) so a client that lives across a long sync doesn't use a stale,
/// expired token. Never prompts for UI — suitable for background sync —
/// so throws [DriveNotAuthorizedException] if authorization isn't already
/// in place (the caller should treat that as "Drive is disconnected").
http.Client authenticatedHttpClient() {
final account = _account;
if (account == null) {
throw DriveNotAuthorizedException();
}
return _DriveHttpClient(account.authorizationClient);
}
}
class _DriveHttpClient extends http.BaseClient {
final GoogleSignInAuthorizationClient _authClient;
final http.Client _inner = http.Client();
_DriveHttpClient(this._authClient);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final headers =
await _authClient.authorizationHeaders(driveScopes, promptIfNecessary: false);
if (headers == null) {
throw DriveNotAuthorizedException();
}
request.headers.addAll(headers);
return _inner.send(request);
}
@override
void close() {
_inner.close();
super.close();
}
}