DEV Community

May Lau
May Lau

Posted on • Updated on • Originally published at maylau.hashnode.dev

Google Drive App Data Backup

Backing up app data is a common action for mobile app. This article will show the steps about backing up Android and IOS files to Google Drive.

Google SignIn

The app data read and write scope require Google Account SignIn. The detail implementation may reference to Google SignIn without Firebase.

Enable Drive API

Goto Enabled APIs & Services on the side menu and click + ENABLE APIS AND SERVICES.

Enabled APIs & Services

Search google drive api.

Search google drive api

Search google drive api

Click Enable Google Drive API.

Enable Google Drive API

Flutter Project Config

Run flutter pub add googleapis to add googleapis plugin to the project.

Create a class called GoogleDriveAppData. The sign in logic will also put inside this class. Since we are going to use the app data storage in Google Drive, we will add the scope to drive.DriveApi.driveAppdataScope.

import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/drive/v3.dart' as drive;

class GoogleDriveAppData {
  /// sign in with google
  Future<GoogleSignInAccount?> signInGoogle() async {
    GoogleSignInAccount? googleUser;
    try {
      GoogleSignIn googleSignIn = GoogleSignIn(
        scopes: [
          drive.DriveApi.driveAppdataScope,
        ],
      );

      googleUser =
          await googleSignIn.signInSilently() ?? await googleSignIn.signIn();
    } catch (e) {
      debugPrint(e.toString());
    }
    return googleUser;
  }

  ///sign out from google
  Future<void> signOut() async {
    GoogleSignIn googleSignIn = GoogleSignIn();
    await googleSignIn.signOut();
  }
}
Enter fullscreen mode Exit fullscreen mode

To use the drive API with its client, we will create a GoogleAuthClient class for passing the auth info with the drive API request.

Run flutter pub add http to import the http package.

import 'package:http/http.dart' as http;

class GoogleAuthClient extends http.BaseClient {
  final Map<String, String> _headers;
  final _client = http.Client();

  GoogleAuthClient(this._headers);

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) {
    request.headers.addAll(_headers);
    return _client.send(request);
  }
}
Enter fullscreen mode Exit fullscreen mode

Go back to the GoogleDriveAppData class.

Add a function to create the Drive client instance.

import 'package:google_drive_app_data/google_auth_client.dart';

class GoogleDriveAppData {
    ...
    ///get google drive client
  Future<drive.DriveApi?> getDriveApi(GoogleSignInAccount googleUser) async {
    drive.DriveApi? driveApi;
    try {
      Map<String, String> headers = await googleUser.authHeaders;
      GoogleAuthClient client = GoogleAuthClient(headers);
      driveApi = drive.DriveApi(client);
    } catch (e) {
      debugPrint(e.toString());
    }
    return driveApi;
  }
}
Enter fullscreen mode Exit fullscreen mode

Google Drive has an app data folder especially for the app’s file storage. The file will save to the appDataFolder.

Run flutter pub add path to add path package for easily get file’s base name.

drive.File is storing the file’s information the drive having.

io.File is the io file which is the local file the device having.

If the file is previously uploaded to the drive, we can use the driveFileId to update the latest version file to the drive.

import 'dart:io' as io;

import 'package:path/path.dart' as path;

class GoogleDriveAppData {
    ...
    /// upload file to google drive
  Future<drive.File?> uploadDriveFile({
    required drive.DriveApi driveApi,
    required io.File file,
    String? driveFileId,
  }) async {
    try {
      drive.File fileMetadata = drive.File();
      fileMetadata.name = path.basename(file.absolute.path);

      late drive.File response;
      if (driveFileId != null) {
        /// [driveFileId] not null means we want to update existing file
        response = await driveApi.files.update(
          fileMetadata,
          driveFileId,
          uploadMedia: drive.Media(file.openRead(), file.lengthSync()),
        );
      } else {
        /// [driveFileId] is null means we want to create new file
        fileMetadata.parents = ['appDataFolder'];
        response = await driveApi.files.create(
          fileMetadata,
          uploadMedia: drive.Media(file.openRead(), file.lengthSync()),
        );
      }
      return response;
    } catch (e) {
      debugPrint(e.toString());
      return null;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

To get the file information of drive like id, version and modified time, we can use the following function to get the file list and compare the filename with the uploaded files.

class GoogleDriveAppData {
    ...
    /// get drive file info
  Future<drive.File?> getDriveFile(
      drive.DriveApi driveApi, String filename) async {
    try {
      drive.FileList fileList = await driveApi.files.list(
          spaces: 'appDataFolder', $fields: 'files(id, name, modifiedTime)');
      List<drive.File>? files = fileList.files;
      drive.File? driveFile =
          files?.firstWhere((element) => element.name == filename);
      return driveFile;
    } catch (e) {
      debugPrint(e.toString());
      return null;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

To download the drive file to local, we can use the following function. Basically download the data stream and write to a new file.

class GoogleDriveAppData {
    ...
    /// download file from google drive
  Future<io.File?> restoreDriveFile({
    required drive.DriveApi driveApi,
    required drive.File driveFile,
    required String targetLocalPath,
  }) async {
    try {
      drive.Media media = await driveApi.files.get(driveFile.id!,
          downloadOptions: drive.DownloadOptions.fullMedia) as drive.Media;

      List<int> dataStore = [];

      await media.stream.forEach((element) {
        dataStore.addAll(element);
      });

      io.File file = io.File(targetLocalPath);
      file.writeAsBytesSync(dataStore);

      return file;
    } catch (e) {
      debugPrint(e.toString());
      return null;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The complete GoogleDriveAppData class will be like this:

import 'dart:io' as io;

import 'package:flutter/foundation.dart';
import 'package:google_drive_app_data/google_auth_client.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/drive/v3.dart' as drive;
import 'package:path/path.dart' as path;

class GoogleDriveAppData {
  /// sign in with google
  Future<GoogleSignInAccount?> signInGoogle() async {
    GoogleSignInAccount? googleUser;
    try {
      GoogleSignIn googleSignIn = GoogleSignIn(
        scopes: [
          drive.DriveApi.driveAppdataScope,
        ],
      );

      googleUser =
          await googleSignIn.signInSilently() ?? await googleSignIn.signIn();
    } catch (e) {
      debugPrint(e.toString());
    }
    return googleUser;
  }

  ///sign out from google
  Future<void> signOut() async {
    GoogleSignIn googleSignIn = GoogleSignIn();
    await googleSignIn.signOut();
  }

  ///get google drive client
  Future<drive.DriveApi?> getDriveApi(GoogleSignInAccount googleUser) async {
    drive.DriveApi? driveApi;
    try {
      Map<String, String> headers = await googleUser.authHeaders;
      GoogleAuthClient client = GoogleAuthClient(headers);
      driveApi = drive.DriveApi(client);
    } catch (e) {
      debugPrint(e.toString());
    }
    return driveApi;
  }

  /// upload file to google drive
  Future<drive.File?> uploadDriveFile({
    required drive.DriveApi driveApi,
    required io.File file,
    String? driveFileId,
  }) async {
    try {
      drive.File fileMetadata = drive.File();
      fileMetadata.name = path.basename(file.absolute.path);

      late drive.File response;
      if (driveFileId != null) {
        /// [driveFileId] not null means we want to update existing file
        response = await driveApi.files.update(
          fileMetadata,
          driveFileId,
          uploadMedia: drive.Media(file.openRead(), file.lengthSync()),
        );
      } else {
        /// [driveFileId] is null means we want to create new file
        fileMetadata.parents = ['appDataFolder'];
        response = await driveApi.files.create(
          fileMetadata,
          uploadMedia: drive.Media(file.openRead(), file.lengthSync()),
        );
      }
      return response;
    } catch (e) {
      debugPrint(e.toString());
      return null;
    }
  }

  /// download file from google drive
  Future<io.File?> restoreDriveFile({
    required drive.DriveApi driveApi,
    required drive.File driveFile,
    required String targetLocalPath,
  }) async {
    try {
      drive.Media media = await driveApi.files.get(driveFile.id!,
          downloadOptions: drive.DownloadOptions.fullMedia) as drive.Media;

      List<int> dataStore = [];

      await media.stream.forEach((element) {
        dataStore.addAll(element);
      });

      io.File file = io.File(targetLocalPath);
      file.writeAsBytesSync(dataStore);

      return file;
    } catch (e) {
      debugPrint(e.toString());
      return null;
    }
  }

  /// get drive file info
  Future<drive.File?> getDriveFile(
      drive.DriveApi driveApi, String filename) async {
    try {
      drive.FileList fileList = await driveApi.files.list(
          spaces: 'appDataFolder', $fields: 'files(id, name, modifiedTime)');
      List<drive.File>? files = fileList.files;
      drive.File? driveFile =
          files?.firstWhere((element) => element.name == filename);
      return driveFile;
    } catch (e) {
      debugPrint(e.toString());
      return null;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Result

Let’s make a simple widget with sign in and call the upload function.

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final GoogleDriveAppData _googleDriveAppData = GoogleDriveAppData();
  GoogleSignInAccount? _googleUser;
  drive.DriveApi? _driveApi;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            GoogleAuthButton(
              onPressed: () async {
                if (_googleUser == null) {
                  _googleUser = await _googleDriveAppData.signInGoogle();
                  if (_googleUser != null) {
                    _driveApi =
                        await _googleDriveAppData.getDriveApi(_googleUser!);
                  }
                } else {
                  await _googleDriveAppData.signOut();
                  _googleUser = null;
                  _driveApi = null;
                }
                setState(() {});
              },
            ),
            ElevatedButton(
              onPressed: _driveApi != null
                  ? () {
                      FilePicker.platform.pickFiles().then((value) {
                        if (value != null && value.files[0] != null) {
                          File selectedFile = File(value.files[0].path!);
                          _googleDriveAppData.uploadDriveFile(
                            driveApi: _driveApi!,
                            file: selectedFile,
                          );
                        }
                      });
                    }
                  : null,
              child: Text('Save sth to drive'),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

app

After the upload success, if you goto the drive web portal and click setting.

drive portal

Choose Manage Apps from the side bar, you will find the app appear in the app list.

Manage Apps

Notice: if you signed in once in the previous article which means your account does not included the drive app data permission may causing 403 when upload file. You need to sign out once and sign in or other methods to let the account allow the app data permission.

Support me if you like the content🍖
ko-fi

Connect🍻
GitHub - MayLau-CbL

Twitter - @MayLauDev

Hashnode - @MayLau

Top comments (1)

Collapse
 
mjolnirph profile image
mjolnirPH

After I see the app in the App List, what's next? Because I only see two options - Disconnect from Drive and Delete Hidden App Data. And the file still isn't uploaded to the drive.

Thanks for this tutorial!

-- Flutter & GDrive Noob