Flutter Upload data to Google Drive

eye-catchDart and Flutter
Sponsored links

We implemented Google login in the previous post. If you haven’t checked the post yet, go to the following post first to learn how to login to Google account.
https://www.technicalfeeder.com/2021/12/flutter-google-login-with-firebase/

We will implement uploading data to Google Drive in this post. Google Drive offers not only normal folders but also hidden folders for our own application that can’t be seen from a user. We will implement both of them in this post.

As always, complete code can be found in my repository.

flutter_samples/google_drive.dart at main · yuto-yuto/flutter_samples
Contribute to yuto-yuto/flutter_samples development by creating an account on GitHub.
Sponsored links

Create a GoogleAuthClient to send additional headers

First of all, we need a google user to get auth information. We can get the info in this code below. The signIn can fail because of network issues. Show an error dialog if it fails.

Future<drive.DriveApi?> _getDriveApi() async {
  final googleUser = await googleSignIn.signIn();
  final headers = await googleUser?.authHeaders;
  if (headers == null) {
    await showMessage(context, "Sign-in first", "Error");
    return null;
  }

  final client = GoogleAuthClient(headers);
  final driveApi = drive.DriveApi(client);
  return driveApi;
}

Once we got the auth info, we need to pass it to the client. DriveApi requires a client to communicate. We need to send the auth info while processing. Let’s create the following class to add the auth info every time when the client sends data to Google Drive.

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

  GoogleAuthClient(this._headers);

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) {
    request.headers.addAll(_headers);
    return _client.send(request);
  }
}
Sponsored links

Upload data to hidden app directory

Each application can use its own folder that is hidden from a user. A user can’t manipulate the files that the application creates. To use the hidden folder, we must set drive.DriveApi.driveAppdataScope in advance.

final googleSignIn = GoogleSignIn.standard(scopes: [
      drive.DriveApi.driveAppdataScope,
]);

Show a progress indicator if we don’t want to allow a user to do something else while the process is ongoing. Don’t forget to remove the dialog at the end within finally block.

Future<void> _uploadToHidden() async {
  try {
    final driveApi = await _getDriveApi();
    if (driveApi == null) {
      return;
    }
    // Not allow a user to do something else
    showGeneralDialog(
      context: context,
      barrierDismissible: false,
      transitionDuration: Duration(seconds: 2),
      barrierColor: Colors.black.withOpacity(0.5),
      pageBuilder: (context, animation, secondaryAnimation) => Center(
        child: CircularProgressIndicator(),
      ),
    );
    ...
  } finally {
    // Remove a dialog
    Navigator.pop(context);
  }
}

Once we got a DriveApi instance, it’s easy to create a file. Normally, an app should find a path to the target files for the backup but this code creates the content to keep it simple. This is the code to create the content.

// Create data here instead of loading a file
final contents = "Technical Feeder";
final Stream<List<int>> mediaStream =
    Future.value(contents.codeUnits).asStream().asBroadcastStream();
var media = new drive.Media(mediaStream, contents.length);

The next step is to set the file info. If we create a file in a app folder, appDataFolder must be specified to parents property. If we don’t want to have multiple backups in the same folder, we need the logic to delete the existing files first.

// Set up File info
var driveFile = new drive.File();
final timestamp = DateFormat("yyyy-MM-dd-hhmmss").format(DateTime.now());
driveFile.name = "technical-feeder-$timestamp.txt";
driveFile.modifiedTime = DateTime.now().toUtc();
driveFile.parents = ["appDataFolder"];

The last step to create a file is to call create method with the info created above.

// Upload
final response = await driveApi.files.create(driveFile, uploadMedia: media);

Let’s check the Google Drive! The file doesn’t appear on the top page.

no-data-in-drive

But if we go to settings page, flutter_samples is there and it owns small disk space.

drive-setting

Get file list from hidden folder

We don’t know if the files are really there without seeing actual data. Let’s add the feature.

Future<void> _showList() async {
  final driveApi = await _getDriveApi();
  if (driveApi == null) {
    return;
  }

  final fileList = await driveApi.files.list(
      spaces: 'appDataFolder', $fields: 'files(id, name, modifiedTime)');
  final files = fileList.files;
  if (files == null) {
    return showMessage(context, "Data not found", "");
  }

  final alert = AlertDialog(
    title: Text("Item List"),
    content: SingleChildScrollView(
      child: ListBody(
        children: files.map((e) => Text(e.name ?? "no-name")).toList(),
      ),
    ),
  );

  return showDialog(
    context: context,
    builder: (BuildContext context) => alert,
  );
}

The main code to get the file list is only 3 lines. We need to specify appDataFolder to spaces to get the list from app own folder as I mentioned above. Specify to $fields property what we need to use.

If you are not familiar with Dialog, check the following post as well.
https://www.technicalfeeder.com/2021/11/flutter-dialog-examples/

The result looks like this.

file-list

Upload data to normal folder

To access to the normal places where it appears to a user, we need to specify the following scope.

final googleSignIn = GoogleSignIn.standard(scopes: [
  drive.DriveApi.driveFileScope,
]);

If it is not specified there, the following error occurs.

// I/flutter ( 6132): DetailedApiRequestError(status: 403, message: The granted scopes do not give access to all of the requested spaces.)

Check if a folder exists in Google Drive, otherwise create it

If we want to upload a file to a folder, we need to check if it already exists. If not, create it and return the folder ID.

Future<String?> _getFolderId(drive.DriveApi driveApi) async {
  final mimeType = "application/vnd.google-apps.folder";
  String folderName = "Flutter-sample-by-tf";

  try {
    final found = await driveApi.files.list(
      q: "mimeType = '$mimeType' and name = '$folderName'",
      $fields: "files(id, name)",
    );
    final files = found.files;
    if (files == null) {
      await showMessage(context, "Sign-in first", "Error");
      return null;
    }

    // The folder already exists
    if (files.isNotEmpty) {
      return files.first.id;
    }

    // Create a folder
    var folder = new drive.File();
    folder.name = folderName;
    folder.mimeType = mimeType;
    final folderCreation = await driveApi.files.create(folder);
    print("Folder ID: ${folderCreation.id}");

    return folderCreation.id;
  } catch (e) {
    print(e);
    return null;
  }
}

The property q is query to search. To search a folder, we need to specify "application/vnd.google-apps.folder" to the mime type. I don’t know when the files property becomes null but it does. Do the error handling.
To create a folder, the process is the same as creating a file. The difference is mime type.

Upload a file to the specific folder

Okay, we can now get the folder ID. Let’s specify the ID to parents property. Other code is the same as before.

Future<void> _uploadToNormal() async {
  try {
    ...
    // Check if the folder exists. If it doesn't exist, create it and return the ID.
    final folderId = await _getFolderId(driveApi);
    if (folderId == null) {
      await showMessage(context, "Failure", "Error");
      return;
    }

    // Create data here instead of loading a file
    final contents = "Technical Feeder";
    final Stream<List<int>> mediaStream =
        Future.value(contents.codeUnits).asStream().asBroadcastStream();
    var media = new drive.Media(mediaStream, contents.length);

    // Set up File info
    var driveFile = new drive.File();
    final timestamp = DateFormat("yyyy-MM-dd-hhmmss").format(DateTime.now());
    driveFile.name = "technical-feeder-$timestamp.txt";
    driveFile.modifiedTime = DateTime.now().toUtc();

    // !!!!!! Set the folder ID here !!!!!!!!!!!!
    driveFile.parents = [folderId];

    // Upload
    final response =
        await driveApi.files.create(driveFile, uploadMedia: media);
    print("response: $response");

    // simulate a slow process
    await Future.delayed(Duration(seconds: 2));
  } finally {
    // Remove a dialog
    Navigator.pop(context);
  }
}

Let’s check the result.

google-drive-file

Yes, a user can see the file. It is not hidden. We’ve done it!

Comments

Copied title and URL