Google Drive App Data Backup

Google Drive App Data Backup

·

6 min read

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.

%E8%9E%A2%E5%B9%95%E6%88%AA%E5%9C%96_2022-07-28_%E4%B8%8B%E5%8D%881.39.39.png

Search google drive api.

%E8%9E%A2%E5%B9%95%E6%88%AA%E5%9C%96_2022-07-28_%E4%B8%8B%E5%8D%881.40.08.png

%E8%9E%A2%E5%B9%95%E6%88%AA%E5%9C%96_2022-07-28_%E4%B8%8B%E5%8D%881.40.23.png

Click Enable Google Drive API.

%E8%9E%A2%E5%B9%95%E6%88%AA%E5%9C%96_2022-07-28_%E4%B8%8B%E5%8D%881.42.21.png

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();
  }
}

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);
  }
}

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;
  }
}

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;
    }
  }
}

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;
    }
  }
}

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;
    }
  }
}

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;
    }
  }
}

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'),
            ),
          ],
        ),
      ),
    );
  }
}

%E8%9E%A2%E5%B9%95%E6%88%AA%E5%9C%96_2022-07-28_%E4%B8%8B%E5%8D%886.27.14.png

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

%E8%9E%A2%E5%B9%95%E6%88%AA%E5%9C%96_2022-07-28_%E4%B8%8B%E5%8D%886.28.12.png

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

%E8%9E%A2%E5%B9%95%E6%88%AA%E5%9C%96_2022-07-28_%E4%B8%8B%E5%8D%886.28.00.png

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.