플러터
[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
설명 :
- 구글 로그인을 한다.
- Upload File 버튼을 누르면 파일 피커가 나타나고 “1.jpg”을 선택 (1.jpg를 선택 해야만 동작..)
- “1.jpg” 파일이 구글 드라이브에 올라가고,
- Download File 버튼을 누르면
- 다운로드 디렉토리에 “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"
...