플러터

[Flutter] Google Drive 연결

sam-ss 2024. 5. 11. 15:59

앱을 업데이트하는데 구글 드라이브 연결이 필요해서 테스트 하면서 정리한 글이다.

플러터를 사용해 구글드라이브 API를 사용해본다(저장/불러오기)

구글 Sign In 설정

참고 : https://ss-sam.tistory.com/22

구글 드라이브 설정

라이브러리 → “google drive api” 검색 → 사용 클릭

OAuth 동의 화면 설정

OAuth 동의 화면 → 범위 추가 또는 삭제 → “google drive api” 검색 → 체크

 

테스트를 위해 테스트 사용자 추가

코드

Package

  google_sign_in: ^6.2.1 # 필수
  googleapis: ^13.1.0 # 필수
  googleapis_auth: ^1.6.0 # 필수
  file_picker: ^8.0.3
  auth_buttons: ^3.0.2
  path_provider: ^2.1.3
  external_path: ^1.0.3
  fluttertoast: ^8.2.5 

Google Sign In & Google OAuth Scope 설정

Future<GoogleSignInAccount?> signInGoogle() async {
  GoogleSignInAccount? googleUser;
  try {
    GoogleSignIn googleSignIn = GoogleSignIn(
      scopes: [
        drive.DriveApi.driveAppdataScope,
      ],
    );

    googleUser = await googleSignIn.signInSilently() ?? await googleSignIn.signIn();
  } catch (e) {
    print("signInGoogle error : $e");
  }
  return googleUser;
}

  
Future<void> signOut() async {
  GoogleSignIn googleSignIn = GoogleSignIn();
  await googleSignIn.signOut();
}

GoogleAuthClient

드라이브 API 요청할때 인증 정보를 같이 전달하는 코드

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

Get Drive API

Google Drive의 리소스에 접근할 수 있게 하는 코드

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) {
    print("getDriveApi error : $e");
  }
  return driveApi;
}

Upload, Download, Drive File Info

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) {
      response = await driveApi.files.update(
        fileMetadata,
        driveFileId,
        uploadMedia: drive.Media(file.openRead(), file.lengthSync()),
      );
    } else {
      fileMetadata.parents = ['appDataFolder']; // appDataFolder 수정하면 안됨.
      response = await driveApi.files.create(
        fileMetadata,
        uploadMedia: drive.Media(file.openRead(), file.lengthSync()),
      );
    }
    return response;
  } catch (e) {
    print("uploadDriveFile error : $e");
    return null;
  }
}

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) {
    print("restoreDriveFile error : $e");
    return null;
  }
}

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) {
    print("getDriveFile error : $e");
    return null;
  }
}

 

완성 코드

google_auth_client.dart

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

 

google_drive_appdata.dart

import 'package:google_drive_test/google_auth_client.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/drive/v3.dart' as drive;
import 'dart:io' as io;
import 'package:path/path.dart' as path;

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

      googleUser = await googleSignIn.signInSilently() ?? await googleSignIn.signIn();
    } catch (e) {
      print("signInGoogle error : $e");
    }
    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) {
      print("getDriveApi error : $e");
    }
    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) {
      print("uploadDriveFile error : $e");
      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) {
      print("restoreDriveFile error : $e");
      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) {
      print("getDriveFile error : $e");
      return null;
    }
  }
}

 

main.dart

설명 :

  1. 구글 로그인을 한다.
  2. Upload File 버튼을 누르면 파일 피커가 나타나고 “1.jpg”을 선택 (1.jpg를 선택 해야만 동작..)
  3. “1.jpg” 파일이 구글 드라이브에 올라가고,
  4. Download File 버튼을 누르면
  5. 다운로드 디렉토리에 “test.jpg”를 다운 받는다.
import 'dart:io';

import 'package:auth_buttons/auth_buttons.dart';
import 'package:external_path/external_path.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:google_drive_test/google_drive_appdata.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/drive/v3.dart' as drive;
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

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;
  String email = "";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(email),
            GoogleAuthButton(
              onPressed: () async {
                if (_googleUser == null) {
                  _googleUser = await _googleDriveAppData.signInGoogle();
                  if (_googleUser != null) {
                    _driveApi = await _googleDriveAppData.getDriveApi(_googleUser!);
                    setState(() {
                      email = _googleUser!.email;
                    });
                  }
                } 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,
                          ).then((value){
                            Fluttertoast.showToast(
                                msg: "upload complete",
                                toastLength: Toast.LENGTH_SHORT,
                                gravity: ToastGravity.BOTTOM,
                                backgroundColor: Colors.red,
                                textColor: Colors.white,
                                fontSize: 16.0
                            );
                          });
                        }
                      });
                    }
                  : null,
              child: const Text('Upload File'),
            ),
            ElevatedButton(
              onPressed: _driveApi != null
                  ? () async {
                      String? downloadDirPath =
                          await ExternalPath.getExternalStoragePublicDirectory(
                              ExternalPath.DIRECTORY_DOWNLOADS);
                      Directory dir = Directory(downloadDirPath);

                      if (!dir.existsSync()) {
                        downloadDirPath = (await getExternalStorageDirectory())!.path;
                      }

                      drive.File? file = await _googleDriveAppData.getDriveFile(
                          _driveApi!, "1.jpg");
                      _googleDriveAppData.restoreDriveFile(
                          driveApi: _driveApi!,
                          driveFile: file!,
                          targetLocalPath: "${dir.path}/test.jpg").then((value) {
                        Fluttertoast.showToast(
                            msg: "download complete",
                            toastLength: Toast.LENGTH_SHORT,
                            gravity: ToastGravity.BOTTOM,
                            backgroundColor: Colors.red,
                            textColor: Colors.white,
                            fontSize: 16.0
                        );
                      });
                    }
                  : null,
              child: const Text('Download File'),
            ),
          ],
        ),
      ),
    );
  }
}

 

 

파일 리스트를 가져와서 다운받으면 좋겠지만.. 파일 리스트를 받아오는 함수는 권한 없음으로 사용 못했다.

그리고 안드로이드는 파일 읽기/쓰기 권한 설정해야한다. IOS는 테스트 못해봄! 끝!

Android Permission 

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

<application
    android:requestLegacyExternalStorage="true"
    android:preserveLegacyExternalStorage="true"
    ...